= 0 && newFloor < LIBRARY_MAPS.size()) {
+ groundOverlay.setImage(BitmapDescriptorFactory.fromResource(LIBRARY_MAPS.get(newFloor)));
+ this.currentFloor = newFloor;
+ Log.d("Floor Change", "Library: Changed to floor " + newFloor);
+ }
}
+ } catch (Exception ex) {
+ Log.e("SetFloor Error:", ex.toString());
}
+ }
+ /**
+ * 重新启用自动楼层时,立即更新到当前实际楼层
+ * @param actualFloor 当前实际楼层
+ */
+ public void resumeAutoFloor(int actualFloor) {
+ Log.d("Floor Status", "Resuming Auto Floor - Actual Floor: " + actualFloor);
+ setCurrentFloor(actualFloor, true);
}
/**
@@ -138,34 +161,36 @@ public void decreaseFloor(){
* Removes the overlay if user no longer in building
*/
private void setBuildingOverlay() {
- // Try catch block to prevent fatal crashes
try {
- // Setting overlay if in Nucleus and not already set
- if (BuildingPolygon.inNucleus(currentLocation) && !isIndoorMapSet) {
+ if (BuildingPolygon.inNucleus(currentLocation)) {
+ if (!isIndoorMapSet) {
+ currentFloor = 1;
groundOverlay = gMap.addGroundOverlay(new GroundOverlayOptions()
- .image(BitmapDescriptorFactory.fromResource(R.drawable.nucleusg))
+ .image(BitmapDescriptorFactory.fromResource(NUCLEUS_MAPS.get(currentFloor)))
.positionFromBounds(NUCLEUS));
isIndoorMapSet = true;
- // Nucleus has an LG floor so G floor is at index 1
- currentFloor=1;
- floorHeight=NUCLEUS_FLOOR_HEIGHT;
+ floorHeight = NUCLEUS_FLOOR_HEIGHT;
+ Log.d("Overlay", "Nucleus: Initial overlay set to floor " + currentFloor);
+ }
}
- // Setting overlay if in Library and not already set
- else if (BuildingPolygon.inLibrary(currentLocation) && !isIndoorMapSet) {
+ else if (BuildingPolygon.inLibrary(currentLocation)) {
+ if (!isIndoorMapSet) {
+ currentFloor = 0;
groundOverlay = gMap.addGroundOverlay(new GroundOverlayOptions()
- .image(BitmapDescriptorFactory.fromResource(R.drawable.libraryg))
+ .image(BitmapDescriptorFactory.fromResource(LIBRARY_MAPS.get(currentFloor)))
.positionFromBounds(LIBRARY));
isIndoorMapSet = true;
- currentFloor=0;
- floorHeight=LIBRARY_FLOOR_HEIGHT;
+ floorHeight = LIBRARY_FLOOR_HEIGHT;
+ Log.d("Overlay", "Library: Initial overlay set to floor " + currentFloor);
+ }
}
- // Removing overlay if user no longer in area with indoor maps available
else if (!BuildingPolygon.inLibrary(currentLocation) &&
- !BuildingPolygon.inNucleus(currentLocation)&& isIndoorMapSet){
+ !BuildingPolygon.inNucleus(currentLocation) && isIndoorMapSet){
groundOverlay.remove();
isIndoorMapSet = false;
- currentFloor=0;
- }
+ currentFloor = 0;
+ Log.d("Overlay", "Removed overlay");
+ }
} catch (Exception ex) {
Log.e("Error with overlay, Exception:", ex.toString());
}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/MainActivity.java b/app/src/main/java/com/openpositioning/PositionMe/MainActivity.java
new file mode 100644
index 00000000..df3e6d11
--- /dev/null
+++ b/app/src/main/java/com/openpositioning/PositionMe/MainActivity.java
@@ -0,0 +1,672 @@
+package com.openpositioning.PositionMe;
+
+import android.Manifest;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.app.AppCompatDelegate;
+import androidx.appcompat.widget.Toolbar;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.navigation.NavController;
+import androidx.navigation.NavOptions;
+import androidx.navigation.fragment.NavHostFragment;
+import androidx.navigation.ui.AppBarConfiguration;
+import androidx.navigation.ui.NavigationUI;
+import androidx.preference.PreferenceManager;
+
+import com.google.android.gms.maps.model.LatLng;
+import com.openpositioning.PositionMe.sensors.Observer;
+import com.openpositioning.PositionMe.sensors.SensorFusion;
+
+/**
+ * The Main Activity of the application, handling setup, permissions and starting all other fragments
+ * and processes.
+ * The Main Activity takes care of most essential tasks before the app can run. Such as setting up
+ * the views, and enforcing light mode so the colour scheme is consistent. It initialises the
+ * various fragments and the navigation between them, getting the Navigation controller. It also
+ * loads the custom action bar with the set theme and icons, and enables back-navigation. The shared
+ * preferences are also loaded.
+ *
+ * The most important task of the main activity is check and asking for the necessary permissions to
+ * enable the application to use the required hardware devices. This is done through a number of
+ * functions that call the OS, as well as pop-up messages warning the user if permissions are denied.
+ *
+ * Once all permissions are granted, the Main Activity obtains the Sensor Fusion instance and sets
+ * the context, enabling the Fragments to interact with the class without setting it up again.
+ *
+ * @see com.openpositioning.PositionMe.fragments.HomeFragment the initial fragment displayed.
+ * @see R.navigation the navigation graph.
+ * @see SensorFusion the singletion data processing class.
+ *
+ * @author Mate Stodulka
+ * @author Virginia Cangelosi
+ */
+public class MainActivity extends AppCompatActivity implements Observer {
+
+ //region Static variables
+ // Static IDs for permission responses.
+ private static final int REQUEST_ID_WIFI_PERMISSION = 99;
+ private static final int REQUEST_ID_LOCATION_PERMISSION = 98;
+ private static final int REQUEST_ID_READ_WRITE_PERMISSION = 97;
+ private static final int REQUEST_ID_ACTIVITY_PERMISSION = 96;
+ //endregion
+
+ //region Instance variables
+ private NavController navController;
+
+ private SharedPreferences settings;
+ private SensorFusion sensorFusion;
+ private Handler httpResponseHandler;
+ private LatLng lastPosition = null;
+ private boolean hasCalibrated = false;
+
+ //endregion
+
+ //region Activity Lifecycle
+
+ /**
+ * {@inheritDoc}
+ * Forces light mode, sets up the navigation graph, initialises the toolbar with back action on
+ * the nav controller, loads the shared preferences and checks for all permissions necessary.
+ * Sets up a Handler for displaying messages from other classes.
+ */
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
+ setContentView(R.layout.activity_main);
+
+ // Set up navigation and fragments
+ NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
+ navController = navHostFragment.getNavController();
+
+ // Set action bar
+ Toolbar toolbar = findViewById(R.id.main_toolbar);
+ setSupportActionBar(toolbar);
+ toolbar.showOverflowMenu();
+ toolbar.setBackgroundColor(ContextCompat.getColor(getApplicationContext(), R.color.primaryBlue));
+ toolbar.setTitleTextColor(ContextCompat.getColor(getApplicationContext(), R.color.white));
+
+ // Set up back action
+ AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
+ NavigationUI.setupWithNavController(toolbar, navController, appBarConfiguration);
+
+ // Get handle for settings
+ this.settings = PreferenceManager.getDefaultSharedPreferences(this);
+ settings.edit().putBoolean("permanentDeny", false).apply();
+
+ //Check Permissions
+ if(ActivityCompat.checkSelfPermission(this,
+ Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED ||
+ ActivityCompat.checkSelfPermission(this,
+ Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED ||
+ ActivityCompat.checkSelfPermission(this,
+ Manifest.permission.ACCESS_WIFI_STATE) != PackageManager.PERMISSION_GRANTED ||
+ ActivityCompat.checkSelfPermission(this,
+ Manifest.permission.CHANGE_WIFI_STATE) != PackageManager.PERMISSION_GRANTED ||
+ ActivityCompat.checkSelfPermission(this,
+ Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED ||
+ ActivityCompat.checkSelfPermission(this,
+ Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
+ ActivityCompat.checkSelfPermission(this,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
+ ActivityCompat.checkSelfPermission(this,
+ Manifest.permission.ACTIVITY_RECOGNITION) != PackageManager.PERMISSION_GRANTED){
+ askLocationPermissions();
+ }
+ // Handler for global toasts and popups from other classes
+ this.httpResponseHandler = new Handler();
+
+ // 初始化 SensorFusion
+ this.sensorFusion = SensorFusion.getInstance();
+ this.sensorFusion.setContext(getApplicationContext());
+ this.sensorFusion.registerForServerUpdate(this);
+
+ // 立即开始监听传感器
+ this.sensorFusion.resumeListening();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onPause() {
+ super.onPause();
+ //Ensure sensorFusion has been initialised before unregistering listeners
+ if(sensorFusion != null) {
+ sensorFusion.stopListening();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * Checks for activities in case the app was closed without granting them, or if they were
+ * granted through the settings page. Repeats the startup checks done in
+ * {@link MainActivity#onCreate(Bundle)}. Starts listening in the SensorFusion class.
+ *
+ * @see SensorFusion the main data processing class.
+ */
+ @Override
+ public void onResume() {
+ super.onResume();
+ //Check if permissions are granted before resuming listeners
+ if(ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
+ != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission
+ (this, Manifest.permission.ACCESS_COARSE_LOCATION)
+ != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission
+ (this,Manifest.permission.ACCESS_WIFI_STATE)
+ != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission
+ (this,Manifest.permission.CHANGE_WIFI_STATE)
+ != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission
+ (this,Manifest.permission.INTERNET)
+ != PackageManager.PERMISSION_GRANTED){
+ askLocationPermissions();
+ }
+ //If permissions are granted resume listeners
+ else {
+ if(sensorFusion == null) {
+ allPermissionsObtained();
+ }
+ else{
+ sensorFusion.resumeListening();
+ }
+ }
+ }
+
+ /**
+ * Unregisters sensor listeners when the app closes. Not in {@link MainActivity#onPause()} to
+ * enable recording data with a locked screen.
+ *
+ * @see SensorFusion the main data processing class.
+ */
+ @Override
+ protected void onDestroy() {
+ if(sensorFusion != null) {
+ sensorFusion.stopListening();
+ }
+ super.onDestroy();
+ }
+
+ //endregion
+
+ //region Permissions
+
+ /**
+ * Checks for location permissions.
+ * If location permissions are not present, request the permissions through the OS.
+ * If permissions are present, check for the next set of required permissions with
+ * {@link MainActivity#askWifiPermissions()}
+ *
+ * @see MainActivity#onRequestPermissionsResult(int, String[], int[]) handling request responses.
+ */
+ private void askLocationPermissions() {
+ // Check for location permission
+ int coarseLocationPermission = ActivityCompat.checkSelfPermission(this,
+ Manifest.permission.ACCESS_COARSE_LOCATION);
+ int fineLocationPermission = ActivityCompat.checkSelfPermission(this,
+ Manifest.permission.ACCESS_FINE_LOCATION);
+ int internetPermission = ActivityCompat.checkSelfPermission(this,
+ Manifest.permission.INTERNET);
+
+ // Request if not present
+ if(coarseLocationPermission != PackageManager.PERMISSION_GRANTED ||
+ fineLocationPermission != PackageManager.PERMISSION_GRANTED ||
+ internetPermission != PackageManager.PERMISSION_GRANTED) {
+ this.requestPermissions(
+ new String[]{
+ Manifest.permission.ACCESS_COARSE_LOCATION,
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.INTERNET},
+ REQUEST_ID_LOCATION_PERMISSION
+ );
+ }
+ else{
+ // Check other permissions if present
+ askWifiPermissions();
+ }
+ }
+
+ /**
+ * Checks for wifi permissions.
+ * If wifi permissions are not present, request the permissions through the OS.
+ * If permissions are present, check for the next set of required permissions with
+ * {@link MainActivity#askStoragePermission()}
+ *
+ * @see MainActivity#onRequestPermissionsResult(int, String[], int[]) handling request responses.
+ */
+ private void askWifiPermissions() {
+ // Check for wifi permissions
+ int wifiAccessPermission = ActivityCompat.checkSelfPermission(this,
+ Manifest.permission.ACCESS_WIFI_STATE);
+ int wifiChangePermission = ActivityCompat.checkSelfPermission(this,
+ Manifest.permission.CHANGE_WIFI_STATE);
+
+ // Request if not present
+ if(wifiAccessPermission != PackageManager.PERMISSION_GRANTED ||
+ wifiChangePermission != PackageManager.PERMISSION_GRANTED) {
+ requestPermissions(
+ new String[]{Manifest.permission.ACCESS_WIFI_STATE,
+ Manifest.permission.CHANGE_WIFI_STATE},
+ REQUEST_ID_WIFI_PERMISSION
+ );
+ }
+ else{
+ // Check other permissions if present
+ askMotionPermissions();
+ }
+ }
+
+ /**
+ * Checks for storage permissions.
+ * If storage permissions are not present, request the permissions through the OS.
+ * If permissions are present, check for the next set of required permissions with
+ * {@link MainActivity#askMotionPermissions()}
+ *
+ * @see MainActivity#onRequestPermissionsResult(int, String[], int[]) handling request responses.
+ */
+ private void askStoragePermission() {
+ // Check for storage permission
+ int writeStoragePermission = ActivityCompat.checkSelfPermission(this,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE);
+ int readStoragePermission = ActivityCompat.checkSelfPermission(this,
+ Manifest.permission.READ_EXTERNAL_STORAGE);
+ // Request if not present
+ if(writeStoragePermission != PackageManager.PERMISSION_GRANTED ||
+ readStoragePermission != PackageManager.PERMISSION_GRANTED) {
+ this.requestPermissions(
+ new String[]{
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ Manifest.permission.READ_EXTERNAL_STORAGE},
+ REQUEST_ID_READ_WRITE_PERMISSION
+ );
+ }
+ else {
+ // Check other permissions if present
+ askMotionPermissions();
+ }
+ }
+
+ /**
+ * Checks for motion activity permissions.
+ * If storage permissions are not present, request the permissions through the OS.
+ * If permissions are present, all permissions have been granted, move on to
+ * {@link MainActivity#allPermissionsObtained()} to initialise SensorFusion.
+ *
+ * @see MainActivity#onRequestPermissionsResult(int, String[], int[]) handling request responses.
+ */
+ private void askMotionPermissions() {
+ // Check for motion activity permission
+ if(Build.VERSION.SDK_INT >= 29) {
+ int activityPermission = ActivityCompat.checkSelfPermission(this,
+ Manifest.permission.ACTIVITY_RECOGNITION);
+ // Request if not present
+ if(activityPermission != PackageManager.PERMISSION_GRANTED) {
+ this.requestPermissions(
+ new String[]{
+ Manifest.permission.ACTIVITY_RECOGNITION},
+ REQUEST_ID_ACTIVITY_PERMISSION
+ );
+ }
+ // Move to finishing function if present
+ else allPermissionsObtained();
+ }
+
+ else allPermissionsObtained();
+ }
+
+ /**
+ * {@inheritDoc}
+ * When a new set of permissions are granted, move on to the next on in the chain of permissions.
+ * Once all permissions are granted, call {@link MainActivity#allPermissionsObtained()}. If any
+ * permissions are denied display 1st time warning pop-up message as the application cannot
+ * function without the required permissions. If permissions are denied twice, display a new
+ * pop-up message, as the OS will not ask for them again, and the user will need to enter the
+ * app settings menu.
+ *
+ * @see MainActivity#askLocationPermissions() first permission request function in the chain.
+ * @see MainActivity#askWifiPermissions() second permission request function in the chain.
+ * @see MainActivity#askStoragePermission() third permission request function in the chain.
+ * @see MainActivity#askMotionPermissions() last permission request function in the chain.
+ * @see MainActivity#allPermissionsObtained() once all permissions are granted.
+ * @see MainActivity#permissionsDeniedFirst() display first pop-up message.
+ * @see MainActivity#permissionsDeniedPermanent() permissions denied twice, pop-up with link to
+ * the appropiate settings menu.
+ */
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ switch (requestCode) {
+ case REQUEST_ID_LOCATION_PERMISSION: { // Location permissions
+ // If request is cancelled results are empty
+ if (grantResults.length > 1 &&
+ grantResults[0] == PackageManager.PERMISSION_GRANTED &&
+ grantResults[1] == PackageManager.PERMISSION_GRANTED &&
+ grantResults[2] == PackageManager.PERMISSION_GRANTED) {
+ Toast.makeText(this, "Location permissions granted!", Toast.LENGTH_SHORT).show();
+ this.settings.edit().putBoolean("gps", true).apply();
+ askWifiPermissions();
+ }
+ else {
+ if(!settings.getBoolean("permanentDeny", false)) {
+ permissionsDeniedFirst();
+ }
+ else permissionsDeniedPermanent();
+ Toast.makeText(this, "Location permissions denied!", Toast.LENGTH_SHORT).show();
+ // Unset setting
+ this.settings.edit().putBoolean("gps", false).apply();
+ }
+ break;
+
+ }
+ case REQUEST_ID_WIFI_PERMISSION: { // Wifi permissions
+ // If request is cancelled results are empty
+ if (grantResults.length > 1 &&
+ grantResults[0] == PackageManager.PERMISSION_GRANTED &&
+ grantResults[1] == PackageManager.PERMISSION_GRANTED) {
+ Toast.makeText(this, "Permissions granted!", Toast.LENGTH_SHORT).show();
+ this.settings.edit().putBoolean("wifi", true).apply();
+ askStoragePermission();
+ }
+ else {
+ if(!settings.getBoolean("permanentDeny", false)) {
+ permissionsDeniedFirst();
+ }
+ else permissionsDeniedPermanent();
+ Toast.makeText(this, "Wifi permissions denied!", Toast.LENGTH_SHORT).show();
+ // Unset setting
+ this.settings.edit().putBoolean("wifi", false).apply();
+ }
+ break;
+ }
+ case REQUEST_ID_READ_WRITE_PERMISSION: { // Read write permissions
+ // If request is cancelled results are empty
+ if (grantResults.length > 1 &&
+ grantResults[0] == PackageManager.PERMISSION_GRANTED &&
+ grantResults[1] == PackageManager.PERMISSION_GRANTED) {
+ Toast.makeText(this, "Permissions granted!", Toast.LENGTH_SHORT).show();
+ askMotionPermissions();
+ }
+ else {
+ if(!settings.getBoolean("permanentDeny", false)) {
+ permissionsDeniedFirst();
+ }
+ else permissionsDeniedPermanent();
+ Toast.makeText(this, "Storage permissions denied!", Toast.LENGTH_SHORT).show();
+ }
+ break;
+ }
+ case REQUEST_ID_ACTIVITY_PERMISSION: { // Activity permissions
+ // If request is cancelled results are empty
+ if (grantResults.length >= 1 &&
+ grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ Toast.makeText(this, "Permissions granted!", Toast.LENGTH_SHORT).show();
+ allPermissionsObtained();
+ }
+ else {
+ if(!settings.getBoolean("permanentDeny", false)) {
+ permissionsDeniedFirst();
+ }
+ else permissionsDeniedPermanent();
+ Toast.makeText(this, "Activity permissions denied!", Toast.LENGTH_SHORT).show();
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Displays a pop-up alert the first time the permissions have been denied.
+ * The pop-up explains the purpose of the application and the necessity of the permissions, and
+ * displays two options. If the "Grant permissions" button is clicked, the permission request
+ * chain is restarted. If the "Exit application" button is clicked, the app closes.
+ *
+ * @see MainActivity#askLocationPermissions() the first in the permission request chain.
+ * @see MainActivity#onRequestPermissionsResult(int, String[], int[]) handling permission results.
+ * @see R.string button text resources.
+ */
+ private void permissionsDeniedFirst() {
+ new AlertDialog.Builder(this)
+ .setTitle("Permissions denied")
+ .setMessage("You have denied access to data gathering devices. The primary purpose of this application is to record data.")
+ .setPositiveButton(R.string.grant, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ settings.edit().putBoolean("permanentDeny", true).apply();
+ askLocationPermissions();
+ }
+ })
+ .setNegativeButton(R.string.exit, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ settings.edit().putBoolean("permanentDeny", true).apply();
+ finishAffinity();
+ }
+ })
+ .setIcon(R.mipmap.ic_launcher_simple)
+ .show();
+ }
+
+ /**
+ * Displays a pop-up alert when permissions have been denied twice.
+ * The OS will not ask for permissions again on the application's behalf. The pop-up explains
+ * the purpose of the application and the necessity of the permissions, and displays a button.
+ * When the "Settings" button is clicked, the app opens the relevant settings menu where
+ * permissions can be adjusted through an intent. Otherwise the app must be closed by the user
+ *
+ * @see R.string button text resources.
+ */
+ private void permissionsDeniedPermanent() {
+ AlertDialog alertDialog = new AlertDialog.Builder(this)
+ .setTitle("Permissions are denied, enable them in settings manually")
+ .setMessage("You have denied necessary sensor permissions for the data recording app. You need to manually enable them in your device's settings.")
+ .setCancelable(false)
+ .setPositiveButton("Settings", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ Uri uri = Uri.fromParts("package", getPackageName(), null);
+ intent.setData(uri);
+ startActivityForResult(intent, 1000);
+ }
+ })
+ .setIcon(R.mipmap.ic_launcher_simple)
+ .create();
+ alertDialog.show();
+ }
+
+ /**
+ * Prepares global resources when all permissions are granted.
+ * Resets the permissions tracking boolean in shared preferences, and initialises the
+ * {@link SensorFusion} class with the application context, and registers the main activity to
+ * listen for server responses that SensorFusion receives.
+ *
+ * @see SensorFusion the main data processing class.
+ * @see ServerCommunications the communication class sending and recieving data from the server.
+ */
+ private void allPermissionsObtained() {
+ settings.edit().putBoolean("permanentDeny", false).apply();
+ this.sensorFusion = SensorFusion.getInstance();
+ this.sensorFusion.setContext(getApplicationContext());
+ sensorFusion.resumeListening();
+ }
+
+ //endregion
+
+ //region Navigation
+
+ /**
+ * {@inheritDoc}
+ * Sets desired animations and navigates to {@link com.openpositioning.PositionMe.fragments.SettingsFragment}
+ * when the settings wheel in the action bar is clicked.
+ */
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ if(navController.getCurrentDestination().getId() == item.getItemId())
+ return super.onOptionsItemSelected(item);
+ else {
+ NavOptions options = new NavOptions.Builder()
+ .setLaunchSingleTop(true)
+ .setEnterAnim(R.anim.slide_in_bottom)
+ .setExitAnim(R.anim.slide_out_top)
+ .setPopEnterAnim(R.anim.slide_in_top)
+ .setPopExitAnim(R.anim.slide_out_bottom).build();
+ navController.navigate(R.id.action_global_settingsFragment, null, options);
+ return true;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * Enables navigating back between fragments.
+ */
+ @Override
+ public boolean onSupportNavigateUp() {
+ return navController.navigateUp() || super.onSupportNavigateUp();
+ }
+
+ /**
+ * {@inheritDoc}
+ * Inflate the designed menu view.
+ *
+ * @see R.menu for the xml file.
+ */
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_items, menu);
+ return true;
+ }
+
+ //endregion
+
+ //region Global toasts
+
+ /**
+ * {@inheritDoc}
+ * 处理来自SensorFusion和ServerCommunications的更新
+ */
+ @Override
+ public void update(Object[] obj) {
+ if (obj.length > 0) {
+ if (obj[0] instanceof Boolean) {
+ // 处理服务器响应
+ if ((Boolean) obj[0]) {
+ this.httpResponseHandler.post(displayToastTaskSuccess);
+ } else {
+ this.httpResponseHandler.post(displayToastTaskFailure);
+ }
+ } else if (sensorFusion != null) {
+ // 检查位置更新
+ float lat = sensorFusion.getLatitude();
+ float lon = sensorFusion.getLongitude();
+ if (lat != 0 && lon != 0) { // 确保有有效的位置
+ checkBuildingEntry(new LatLng(lat, lon));
+ }
+ }
+ }
+ }
+
+ /**
+ * Task that displays positive toast on the main UI thread.
+ * Called when {@link ServerCommunications} successfully uploads a trajectory.
+ */
+ private final Runnable displayToastTaskSuccess = new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(MainActivity.this, "Trajectory uploaded", Toast.LENGTH_SHORT).show();
+ }
+ };
+
+ /**
+ * Task that displays negative toast on the main UI thread.
+ * Called when {@link ServerCommunications} fails to upload a trajectory.
+ */
+ private final Runnable displayToastTaskFailure = new Runnable() {
+ @Override
+ public void run() {
+// Toast.makeText(MainActivity.this, "Failed to complete trajectory upload", Toast.LENGTH_SHORT).show();
+ }
+ };
+
+ //endregion
+
+ /**
+ * 检查并处理建筑物进入事件
+ * @param newPosition 新的位置
+ */
+ private void checkBuildingEntry(LatLng newPosition) {
+ if (lastPosition == null) {
+ lastPosition = newPosition;
+ return;
+ }
+
+ // 检查是否刚进入建筑物
+ boolean wasInBuilding = BuildingPolygon.inNucleus(lastPosition) ||
+ BuildingPolygon.inLibrary(lastPosition);
+ boolean isInBuilding = BuildingPolygon.inNucleus(newPosition) ||
+ BuildingPolygon.inLibrary(newPosition);
+
+ if (!wasInBuilding && isInBuilding && !hasCalibrated) {
+ // 刚进入建筑物,进行校准
+ if (BuildingPolygon.inNucleus(newPosition)) {
+ // Nucleus大楼,在G层(0层)校准
+ sensorFusion.calibrateAtKnownFloor(0);
+ Log.d("CALIBRATION", "Enter Nucleus, callibrate in G layer");
+ } else if (BuildingPolygon.inLibrary(newPosition)) {
+ // 图书馆,在G层(0层)校准
+ sensorFusion.calibrateAtKnownFloor(0);
+ Log.d("CALIBRATION", "Enter Library, callibrate in G layer");
+ }
+ hasCalibrated = true;
+
+ // 显示校准提示
+ runOnUiThread(() -> {
+ Toast.makeText(this, "Callibrated at current floor", Toast.LENGTH_SHORT).show();
+ });
+ } else if (!isInBuilding) {
+ // 离开建筑物,重置校准标志
+ hasCalibrated = false;
+ }
+
+ lastPosition = newPosition;
+ }
+
+ // 添加手动校准的方法
+ public void calibrateAtCurrentFloor(View view) {
+ if (sensorFusion != null) {
+ LatLng currentPosition = new LatLng(
+ sensorFusion.getLatitude(),
+ sensorFusion.getLongitude()
+ );
+
+ int floorToCalibrate = 0; // 默认在G层校准
+
+ // 根据位置确定校准楼层
+ if (BuildingPolygon.inNucleus(currentPosition)) {
+ // 在Nucleus大楼,可以根据实际情况选择校准楼层
+ floorToCalibrate = 0; // G层
+ } else if (BuildingPolygon.inLibrary(currentPosition)) {
+ // 在图书馆,可以根据实际情况选择校准楼层
+ floorToCalibrate = 0; // G层
+ }
+
+ sensorFusion.calibrateAtKnownFloor(floorToCalibrate);
+ Toast.makeText(this, "Callibrated at current floor", Toast.LENGTH_SHORT).show();
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/PathView.java b/app/src/main/java/com/openpositioning/PositionMe/PathView.java
similarity index 98%
rename from app/src/main/java/com/openpositioning/PositionMe/utils/PathView.java
rename to app/src/main/java/com/openpositioning/PositionMe/PathView.java
index 5a5efa8d..cc8087ff 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/utils/PathView.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/PathView.java
@@ -1,4 +1,4 @@
-package com.openpositioning.PositionMe.utils;
+package com.openpositioning.PositionMe;
import android.content.Context;
import android.graphics.Canvas;
@@ -8,7 +8,7 @@
import android.util.AttributeSet;
import android.view.View;
-import com.openpositioning.PositionMe.presentation.fragment.CorrectionFragment;
+import com.openpositioning.PositionMe.fragments.CorrectionFragment;
import com.openpositioning.PositionMe.sensors.SensorFusion;
import java.util.ArrayList;
diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/PdrProcessing.java b/app/src/main/java/com/openpositioning/PositionMe/PdrProcessing.java
similarity index 54%
rename from app/src/main/java/com/openpositioning/PositionMe/utils/PdrProcessing.java
rename to app/src/main/java/com/openpositioning/PositionMe/PdrProcessing.java
index 9765b044..835b0112 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/utils/PdrProcessing.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/PdrProcessing.java
@@ -1,19 +1,19 @@
-package com.openpositioning.PositionMe.utils;
+package com.openpositioning.PositionMe;
import android.content.Context;
import android.content.SharedPreferences;
import android.hardware.SensorManager;
+import android.util.Log;
import androidx.preference.PreferenceManager;
import com.openpositioning.PositionMe.sensors.SensorFusion;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.OptionalDouble;
-import java.util.Objects;
-import java.util.stream.Collectors;
/**
* Processes data recorded in the {@link SensorFusion} class and calculates live PDR estimates.
@@ -28,16 +28,15 @@ public class PdrProcessing {
//region Static variables
// Weiberg algorithm coefficient for stride calculations
- private static final float K = 0.364f;
+ private static final float K = 0.4f;
// Number of samples (seconds) to keep as memory for elevation calculation
private static final int elevationSeconds = 4;
// Number of samples (0.01 seconds)
private static final int accelSamples = 100;
// Threshold used to detect significant movement
- private static final float movementThreshold = 0.3f; // m/s^2
+ private static final float movementThreshold = 0.4f;
// Threshold under which movement is considered non-existent
- private static final float epsilon = 0.18f;
- private static final int MIN_REQUIRED_SAMPLES = 2;
+ private static final float epsilon = 0.25f;
//endregion
//region Instance variables
@@ -71,6 +70,36 @@ public class PdrProcessing {
// Step sum and length aggregation variables
private float sumStepLength = 0;
private int stepCount = 0;
+
+ // 修改陀螺仪相关变量
+ private float[] gyroBuffer = new float[5]; // 减少到5个样本
+ private int gyroBufferIndex = 0;
+ private float gyroBias = 0;
+ private float lastHeading = 0;
+
+ // 调整卡尔曼滤波器参数以更快响应变化
+ private float headingState = 0;
+ private float headingCovariance = 1.0f;
+ private static final float Q = 0.05f; // 降低过程噪声,使滤波器更稳定
+ private static final float R = 0.15f; // 增加测量噪声,减少传感器噪声影响
+
+ // 磁力计校准相关变量
+ private float[] magnetometerBuffer = new float[10]; // 存储最近10个磁力计读数
+ private int magnetometerIndex = 0;
+ private boolean isMagnetometerCalibrated = false;
+ private static final float MAGNETOMETER_THRESHOLD = 0.5f; // 磁力计数据稳定性阈值
+
+ // 初始方向校准等待时间
+ private static final int CALIBRATION_WAIT_TIME = 2000; // 2秒
+ private long calibrationStartTime = 0;
+ private boolean isCalibrating = false;
+
+ // WGS84椭球体参数
+ private static final double WGS84_A = 6378137.0; // 长半轴
+ private static final double WGS84_F = 1/298.257223563; // 扁率
+ private static final double WGS84_B = WGS84_A * (1 - WGS84_F); // 短半轴
+ private static final double WGS84_E = Math.sqrt(1 - Math.pow(WGS84_B/WGS84_A, 2)); // 第一偏心率
+
//endregion
/**
@@ -131,50 +160,158 @@ public PdrProcessing(Context context) {
}
/**
- * Function to calculate PDR coordinates from sensor values.
- * Should be called from the step detector sensor's event with the sensor values since the last
- * step.
- *
- * @param currentStepEnd relative time in milliseconds since the start of the recording.
- * @param accelMagnitudeOvertime recorded acceleration magnitudes since the last step.
- * @param headingRad heading relative to magnetic north in radians.
+ * 将大地坐标转换为ECEF坐标
+ * @param lat 纬度(弧度)
+ * @param lon 经度(弧度)
+ * @param h 大地高(米)
+ * @return ECEF坐标 [X, Y, Z]
*/
- public float[] updatePdr(long currentStepEnd, List accelMagnitudeOvertime, float headingRad) {
- if (accelMagnitudeOvertime == null || accelMagnitudeOvertime.size() < MIN_REQUIRED_SAMPLES) {
- return new float[]{this.positionX, this.positionY}; // Return current position without update
- // - TODO - temporary solution of the empty list issue
- }
+ private double[] geodetic2ECEF(double lat, double lon, double h) {
+ double N = WGS84_A / Math.sqrt(1 - Math.pow(WGS84_E * Math.sin(lat), 2));
+ double X = (N + h) * Math.cos(lat) * Math.cos(lon);
+ double Y = (N + h) * Math.cos(lat) * Math.sin(lon);
+ double Z = (N * (1 - Math.pow(WGS84_E, 2)) + h) * Math.sin(lat);
+ return new double[]{X, Y, Z};
+ }
+
+ /**
+ * 将ECEF坐标转换为ENU局部坐标系
+ * @param X ECEF X坐标
+ * @param Y ECEF Y坐标
+ * @param Z ECEF Z坐标
+ * @param refLat 参考点纬度(弧度)
+ * @param refLon 参考点经度(弧度)
+ * @param refH 参考点大地高(米)
+ * @return ENU坐标 [E, N, U]
+ */
+ private double[] ECEF2ENU(double X, double Y, double Z, double refLat, double refLon, double refH) {
+ // 计算参考点的ECEF坐标
+ double[] refECEF = geodetic2ECEF(refLat, refLon, refH);
+
+ // 计算相对位置
+ double dX = X - refECEF[0];
+ double dY = Y - refECEF[1];
+ double dZ = Z - refECEF[2];
+
+ // 转换矩阵
+ double sinLat = Math.sin(refLat);
+ double cosLat = Math.cos(refLat);
+ double sinLon = Math.sin(refLon);
+ double cosLon = Math.cos(refLon);
+
+ // 计算ENU坐标
+ double E = -sinLon * dX + cosLon * dY;
+ double N = -sinLat * cosLon * dX - sinLat * sinLon * dY + cosLat * dZ;
+ double U = cosLat * cosLon * dX + cosLat * sinLon * dY + sinLat * dZ;
+
+ return new double[]{E, N, U};
+ }
- // Change angle so zero rad is east
- float adaptedHeading = (float) (Math.PI/2 - headingRad);
+ /**
+ * 更新陀螺仪数据
+ * @param gyroZ 陀螺仪Z轴角速度
+ */
+ public void updateGyro(float gyroZ) {
+ // 更新陀螺仪缓冲区
+ gyroBuffer[gyroBufferIndex] = gyroZ;
+ gyroBufferIndex = (gyroBufferIndex + 1) % gyroBuffer.length;
+
+ // 计算陀螺仪零偏
+ float sum = 0;
+ for (float value : gyroBuffer) {
+ sum += value;
+ }
+ gyroBias = sum / gyroBuffer.length;
+ }
+
+ /**
+ * 卡尔曼滤波处理航向角
+ * @param measurement 测量值(来自磁力计)
+ * @return 滤波后的航向角
+ */
+ private float kalmanFilter(float measurement) {
+ // 预测步骤
+ float predictedState = headingState; // 假设航向角变化不大
+ float predictedCovariance = headingCovariance + Q;
+
+ // 更新步骤
+ float kalmanGain = predictedCovariance / (predictedCovariance + R);
+ headingState = predictedState + kalmanGain * (measurement - predictedState);
+ headingCovariance = (1 - kalmanGain) * predictedCovariance;
+
+ // 标准化航向角到[-π, π]范围
+ while (headingState > Math.PI) headingState -= 2 * Math.PI;
+ while (headingState < -Math.PI) headingState += 2 * Math.PI;
+
+ return headingState;
+ }
- // check if accelMagnitudeOvertime is empty
- if (accelMagnitudeOvertime == null || accelMagnitudeOvertime.isEmpty()) {
- // return current position, do not update
- return new float[]{this.positionX, this.positionY};
+ /**
+ * 更新PDR坐标,包含WGS84转换
+ */
+ public float[] updatePdr(long currentStepEnd, List accelMagnitudeOvertime, float headingRad) {
+ // 初始方向校准
+ if (!isCalibrating) {
+ calibrationStartTime = currentStepEnd;
+ isCalibrating = true;
+ }
+
+ // 在校准期间收集磁力计数据
+ if (isCalibrating && currentStepEnd - calibrationStartTime < CALIBRATION_WAIT_TIME) {
+ // 收集磁力计数据
+ magnetometerBuffer[magnetometerIndex] = headingRad;
+ magnetometerIndex = (magnetometerIndex + 1) % magnetometerBuffer.length;
+
+ // 检查磁力计数据稳定性
+ if (magnetometerIndex == 0) {
+ float variance = calculateVariance(magnetometerBuffer);
+ if (variance < MAGNETOMETER_THRESHOLD) {
+ isMagnetometerCalibrated = true;
+ // 使用稳定的磁力计数据作为初始方向
+ headingState = calculateMean(magnetometerBuffer);
+ headingCovariance = 0.1f; // 降低初始不确定性
+ }
+ }
+ }
+
+ // 使用卡尔曼滤波处理航向角
+ float filteredHeading = kalmanFilter(headingRad);
+
+ // 计算航向角变化
+ float headingChange = filteredHeading - lastHeading;
+ lastHeading = filteredHeading;
+
+ // 限制航向角变化幅度
+ if (Math.abs(headingChange) > Math.PI/2) {
+ headingChange = (float) (Math.signum(headingChange) * Math.PI/2);
}
- // Calculate step length
+ // 使用滤波后的航向角计算位置
+ float adaptedHeading = (float) (Math.PI/2 - filteredHeading);
+
+ // 计算步长
if(!useManualStep) {
- //ArrayList accelMagnitudeFiltered = filter(accelMagnitudeOvertime);
- // Estimate stride
this.stepLength = weibergMinMax(accelMagnitudeOvertime);
- // System.err.println("Step Length" + stepLength);
+ // 限制步长范围
+ if (this.stepLength < 0.25f) {
+ this.stepLength = 0.25f; // 最小步长为0.25米
+ } else if (this.stepLength > 0.75f) {
+ this.stepLength = 0.75f; // 最大步长为0.75米
+ }
}
-
- // Increment aggregate variables
- sumStepLength += stepLength;
- stepCount++;
-
- // Translate to cartesian coordinate system
- float x = (float) (stepLength * Math.cos(adaptedHeading));
- float y = (float) (stepLength * Math.sin(adaptedHeading));
-
- // Update position values
- this.positionX += x;
- this.positionY += y;
-
- // return current position
+
+ // 计算局部ENU坐标系中的位移
+ float deltaE = (float) (stepLength * Math.cos(adaptedHeading));
+ float deltaN = (float) (stepLength * Math.sin(adaptedHeading));
+
+ // 更新位置
+ this.positionX += deltaE;
+ this.positionY += deltaN;
+
+ // 记录步长信息用于调试
+ Log.d("PdrProcessing", String.format("步长: %.2fm, 方向: %.1f°, 位移: dX=%.2f, dY=%.2f",
+ stepLength, (float)Math.toDegrees(adaptedHeading), deltaE, deltaN));
+
return new float[]{this.positionX, this.positionY};
}
@@ -233,32 +370,109 @@ public float updateElevation(float absoluteElevation) {
* @return float stride length in meters.
*/
private float weibergMinMax(List accelMagnitude) {
- // if the list itself is null or empty, return 0 (or return other default values as needed)
+ // 检查列表是否为空,避免应用崩溃
if (accelMagnitude == null || accelMagnitude.isEmpty()) {
- return 0f;
+ Log.w("PdrProcessing", "accelMagnitude 列表为空,weibergMinMax 返回默认值 0.0f");
+ return 0.0f; // 返回默认步长,避免异常
}
- // filter out null values from the list
- List validAccel = accelMagnitude.stream()
- .filter(Objects::nonNull)
- .collect(Collectors.toList());
- if (validAccel.isEmpty()) {
- return 0f;
+ // 对加速度列表进行过滤,减少噪声影响
+ List filteredAccelMagnitude = filterAcceleration(accelMagnitude);
+
+ double maxAccel = Collections.max(filteredAccelMagnitude);
+ double minAccel = Collections.min(filteredAccelMagnitude);
+
+ // 最小加速度差异阈值,避免静止状态下微小振动导致的错误步长计算
+ double accelThreshold = 0.45;
+ if ((maxAccel - minAccel) < accelThreshold) {
+ Log.d("PdrProcessing", String.format("加速度差异(%.2f)低于阈值(%.2f),使用默认步长",
+ (maxAccel - minAccel), accelThreshold));
+ return 0.5f; // 使用默认步长
}
-
- // calculate max and min values
- double maxAccel = Collections.max(validAccel);
- double minAccel = Collections.min(validAccel);
-
- // calculate bounce
+
float bounce = (float) Math.pow((maxAccel - minAccel), 0.25);
+
+ // 输出计算过程,便于调试
+ Log.d("PdrProcessing", String.format("加速度 - 最大: %.2f, 最小: %.2f, 波动值: %.2f",
+ maxAccel, minAccel, bounce));
- // determine which constant to use based on settings
if (this.settings.getBoolean("overwrite_constants", false)) {
- return bounce * Float.parseFloat(settings.getString("weiberg_k", "0.934")) * 2;
+ float customK = Float.parseFloat(settings.getString("weiberg_k", "0.934"));
+ float stepLen = bounce * customK * 2;
+ Log.d("PdrProcessing", "使用自定义K值: " + customK + ", 计算步长: " + stepLen);
+ return stepLen;
}
+
+ float stepLen = bounce * K * 2;
+ Log.d("PdrProcessing", "使用默认K值: " + K + ", 计算步长: " + stepLen);
+ return stepLen;
+ }
- return bounce * K * 2;
+ /**
+ * 过滤加速度数据,减少噪声影响
+ * @param accelMagnitude 原始加速度数据列表
+ * @return 过滤后的加速度数据列表
+ */
+ private List filterAcceleration(List accelMagnitude) {
+ if (accelMagnitude.size() <= 3) {
+ return accelMagnitude; // 数据点太少,不进行过滤
+ }
+
+ // 使用更强的滤波处理
+ List filtered = new ArrayList<>();
+ double sum = 0;
+
+ // 先计算平均值以检测异常值
+ for (Double value : accelMagnitude) {
+ sum += value;
+ }
+ double mean = sum / accelMagnitude.size();
+
+ // 计算标准差
+ double variance = 0;
+ for (Double value : accelMagnitude) {
+ variance += Math.pow(value - mean, 2);
+ }
+ double stdDev = Math.sqrt(variance / accelMagnitude.size());
+ double threshold = stdDev * 2.0; // 设置异常值阈值为2倍标准差
+
+ // 排除异常值并进行中值滤波
+ for (int i = 1; i < accelMagnitude.size() - 1; i++) {
+ // 检查是否是异常值
+ if (Math.abs(accelMagnitude.get(i) - mean) > threshold) {
+ // 异常值用相邻两点的平均值替代
+ filtered.add((accelMagnitude.get(i-1) + accelMagnitude.get(i+1)) / 2);
+ } else {
+ // 非异常值使用中值滤波
+ List window = new ArrayList<>();
+ window.add(accelMagnitude.get(i-1));
+ window.add(accelMagnitude.get(i));
+ window.add(accelMagnitude.get(i+1));
+
+ Collections.sort(window);
+ filtered.add(window.get(1));
+ }
+ }
+
+ // 处理首尾两点
+ if (accelMagnitude.size() > 0) {
+ // 检查首点是否是异常值
+ if (accelMagnitude.size() > 1 && Math.abs(accelMagnitude.get(0) - mean) > threshold) {
+ filtered.add(0, accelMagnitude.get(1)); // 用第二点替代
+ } else {
+ filtered.add(0, accelMagnitude.get(0));
+ }
+
+ // 检查尾点是否是异常值
+ int lastIdx = accelMagnitude.size() - 1;
+ if (accelMagnitude.size() > 1 && Math.abs(accelMagnitude.get(lastIdx) - mean) > threshold) {
+ filtered.add(accelMagnitude.get(lastIdx - 1)); // 用倒数第二点替代
+ } else {
+ filtered.add(accelMagnitude.get(lastIdx));
+ }
+ }
+
+ return filtered;
}
/**
@@ -414,4 +628,28 @@ public float getAverageStepLength(){
return averageStepLength;
}
+ /**
+ * 计算数组的方差
+ */
+ private float calculateVariance(float[] data) {
+ float mean = calculateMean(data);
+ float sumSquaredDiff = 0;
+ for (float value : data) {
+ float diff = value - mean;
+ sumSquaredDiff += diff * diff;
+ }
+ return sumSquaredDiff / data.length;
+ }
+
+ /**
+ * 计算数组的平均值
+ */
+ private float calculateMean(float[] data) {
+ float sum = 0;
+ for (float value : data) {
+ sum += value;
+ }
+ return sum / data.length;
+ }
+
}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java b/app/src/main/java/com/openpositioning/PositionMe/ServerCommunications.java
similarity index 52%
rename from app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java
rename to app/src/main/java/com/openpositioning/PositionMe/ServerCommunications.java
index 7f7e74b2..92bc6603 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/ServerCommunications.java
@@ -1,60 +1,41 @@
-package com.openpositioning.PositionMe.data.remote;
-import android.util.Log;
-import java.util.Map;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.io.BufferedReader;
-import java.io.FileReader;
-import org.json.JSONObject;
-
-import android.os.Environment;
-
-import java.io.FileInputStream;
-import java.io.OutputStream;
+package com.openpositioning.PositionMe;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
-import android.os.Build;
-import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
+import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
-import com.google.protobuf.util.JsonFormat;
-import com.openpositioning.PositionMe.BuildConfig;
-import com.openpositioning.PositionMe.Traj;
-import com.openpositioning.PositionMe.presentation.fragment.FilesFragment;
-import com.openpositioning.PositionMe.presentation.activity.MainActivity;
+import com.openpositioning.PositionMe.fragments.FilesFragment;
import com.openpositioning.PositionMe.sensors.Observable;
import com.openpositioning.PositionMe.sensors.Observer;
-import java.io.ByteArrayOutputStream;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
+import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
+import java.util.Map;
import okhttp3.Call;
-import okhttp3.Callback;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
-import okhttp3.OkHttp;
import okhttp3.OkHttpClient;
-import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
@@ -71,10 +52,9 @@
* @author Mate Stodulka
*/
public class ServerCommunications implements Observable {
- public static Map downloadRecords = new HashMap<>();
+
// Application context for handling permissions and devices
private final Context context;
-
// Network status checking
private ConnectivityManager connMgr;
private boolean isWifiConn;
@@ -85,6 +65,8 @@ public class ServerCommunications implements Observable {
private boolean success;
private List observers;
+ private List> entryList;
+
// Static constants necessary for communications
private static final String userKey = BuildConfig.OPENPOSITIONING_API_KEY;
private static final String masterKey = BuildConfig.OPENPOSITIONING_MASTER_KEY;
@@ -120,6 +102,10 @@ public ServerCommunications(Context context) {
this.observers = new ArrayList<>();
}
+ public void setEntryList(List> entryList) {
+ this.entryList = entryList;
+ }
+
/**
* Outgoing communication request with a {@link Traj trajectory} object. The recorded
* trajectory is passed to the method. It is processed into the right format for sending
@@ -128,26 +114,16 @@ public ServerCommunications(Context context) {
* @param trajectory Traj object matching all the timing and formal restrictions.
*/
public void sendTrajectory(Traj.Trajectory trajectory){
- logDataSize(trajectory);
// Convert the trajectory to byte array
byte[] binaryTrajectory = trajectory.toByteArray();
- File path = null;
- // for android 13 or higher use dedicated external storage
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- path = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
- if (path == null) {
- path = context.getFilesDir();
- }
- } else { // for android 12 or lower use internal storage
- path = context.getFilesDir();
- }
-
- System.out.println(path.toString());
+ // Get the directory path for storing the file with the trajectory
+ File path = context.getFilesDir();
// Format the file name according to date
- SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yy-HH-mm-ss");
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
+ dateFormat.setTimeZone(java.util.TimeZone.getDefault()); // 使用本地时区
Date date = new Date();
File file = new File(path, "trajectory_" + dateFormat.format(date) + ".txt");
@@ -176,16 +152,16 @@ public void sendTrajectory(Traj.Trajectory trajectory){
// Creaet a equest body with a file to upload in multipart/form-data format
RequestBody requestBody = new MultipartBody.Builder().setType(MultipartBody.FORM)
.addFormDataPart("file", file.getName(),
- RequestBody.create(MediaType.parse("text/plain"), file))
+ RequestBody.create(MediaType.parse("application/json"), file))
.build();
// Create a POST request with the required headers
- Request request = new Request.Builder().url(uploadURL).post(requestBody)
+ okhttp3.Request request = new okhttp3.Request.Builder().url(uploadURL).post(requestBody)
.addHeader("accept", PROTOCOL_ACCEPT_TYPE)
.addHeader("Content-Type", PROTOCOL_CONTENT_TYPE).build();
// Enqueue the request to be executed asynchronously and handle the response
- client.newCall(request).enqueue(new Callback() {
+ client.newCall(request).enqueue(new okhttp3.Callback() {
// Handle failure to get response from the server
@Override public void onFailure(Call call, IOException e) {
@@ -197,17 +173,6 @@ public void sendTrajectory(Traj.Trajectory trajectory){
notifyObservers(1);
}
- private void copyFile(File src, File dst) throws IOException {
- try (InputStream in = new FileInputStream(src);
- OutputStream out = new FileOutputStream(dst)) {
- byte[] buf = new byte[1024];
- int len;
- while ((len = in.read(buf)) > 0) {
- out.write(buf, 0, len);
- }
- }
- }
-
// Process the server's response
@Override public void onResponse(Call call, Response response) throws IOException {
try (ResponseBody responseBody = response.body()) {
@@ -218,9 +183,13 @@ private void copyFile(File src, File dst) throws IOException {
// System.err.println("POST error response: " + responseBody.string());
String errorBody = responseBody.string();
- infoResponse = "Upload failed: " + errorBody;
- new Handler(Looper.getMainLooper()).post(() ->
- Toast.makeText(context, infoResponse, Toast.LENGTH_SHORT).show()); // show error message to users
+ infoResponse = "上传失败: " + errorBody;
+ Log.e("ServerCommunications", "上传错误: " + errorBody);
+
+ new Handler(Looper.getMainLooper()).post(() -> {
+ Toast.makeText(context, infoResponse, Toast.LENGTH_LONG).show();
+ Log.e("ServerCommunications", "上传错误: " + errorBody);
+ });
System.err.println("POST error response: " + errorBody);
success = false;
@@ -236,21 +205,6 @@ private void copyFile(File src, File dst) throws IOException {
// Print a confirmation of a successful POST to API
System.out.println("Successful post response: " + responseBody.string());
- System.out.println("Get file: " + file.getName());
- String originalPath = file.getAbsolutePath();
- System.out.println("Original trajectory file saved at: " + originalPath);
-
- // Copy the file to the Downloads folder
- File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
- File downloadFile = new File(downloadsDir, file.getName());
- try {
- copyFile(file, downloadFile);
- System.out.println("Trajectory file copied to Downloads: " + downloadFile.getAbsolutePath());
- } catch (IOException e) {
- e.printStackTrace();
- System.err.println("Failed to copy file to Downloads: " + e.getMessage());
- }
-
// Delete local file and set success to true
success = file.delete();
notifyObservers(1);
@@ -265,37 +219,53 @@ private void copyFile(File src, File dst) throws IOException {
success = false;
notifyObservers(1);
}
+
}
/**
* Uploads a local trajectory file to the API server in the specified format.
- * {@link OkHttp} library is used for the asynchronous POST request.
+ * {@link okhttp3.OkHttp} library is used for the asynchronous POST request.
*
* @param localTrajectory the File object of the local trajectory to be uploaded
*/
public void uploadLocalTrajectory(File localTrajectory) {
+ // 从文件名中提取时间并转换为本地时区
+ String fileName = localTrajectory.getName();
+ File finalTrajectory = localTrajectory; // 初始化为原始文件
+ try {
+ // 提取时间部分
+ String timeStr = fileName.substring(fileName.indexOf("_") + 1, fileName.lastIndexOf("."));
+ SimpleDateFormat serverFormat = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
+ serverFormat.setTimeZone(java.util.TimeZone.getTimeZone("UTC"));
+ java.util.Date utcDate = serverFormat.parse(timeStr);
+
+ // 转换为本地时区
+ SimpleDateFormat localFormat = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
+ localFormat.setTimeZone(java.util.TimeZone.getDefault());
+ String localTimeStr = localFormat.format(utcDate);
+
+ // 更新文件名
+ String newFileName = fileName.replace(timeStr, localTimeStr);
+ File newFile = new File(localTrajectory.getParent(), newFileName);
+ if (!localTrajectory.renameTo(newFile)) {
+ Log.e("ServerCommunications", "Failed to rename file");
+ return;
+ }
+ finalTrajectory = newFile; // 更新为新的文件
+ } catch (Exception e) {
+ Log.e("ServerCommunications", "Error processing timezone", e);
+ // 如果出错,保持使用原始文件
+ }
+
+ final File uploadFile = finalTrajectory; // 创建一个final变量用于匿名内部类
// Instantiate client for HTTP requests
OkHttpClient client = new OkHttpClient();
- // robustness improvement
- RequestBody fileRequestBody;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- try {
- byte[] fileBytes = Files.readAllBytes(localTrajectory.toPath());
- fileRequestBody = RequestBody.create(MediaType.parse("text/plain"), fileBytes);
- } catch (IOException e) {
- e.printStackTrace();
- // if failed, use File object to construct RequestBody
- fileRequestBody = RequestBody.create(MediaType.parse("text/plain"), localTrajectory);
- }
- } else {
- fileRequestBody = RequestBody.create(MediaType.parse("text/plain"), localTrajectory);
- }
-
// Create request body with a file to upload in multipart/form-data format
RequestBody requestBody = new MultipartBody.Builder().setType(MultipartBody.FORM)
- .addFormDataPart("file", localTrajectory.getName(), fileRequestBody)
+ .addFormDataPart("file", uploadFile.getName(),
+ RequestBody.create(MediaType.parse("application/json"), uploadFile))
.build();
// Create a POST request with the required headers
@@ -305,34 +275,26 @@ public void uploadLocalTrajectory(File localTrajectory) {
// Enqueue the request to be executed asynchronously and handle the response
client.newCall(request).enqueue(new okhttp3.Callback() {
- @Override
- public void onFailure(Call call, IOException e) {
+ @Override public void onFailure(Call call, IOException e) {
// Print error message, set success to false and notify observers
e.printStackTrace();
-// localTrajectory.delete();
success = false;
System.err.println("UPLOAD: Failure to get response");
notifyObservers(1);
infoResponse = "Upload failed: " + e.getMessage(); // Store error message
- new Handler(Looper.getMainLooper()).post(() ->
- Toast.makeText(context, infoResponse, Toast.LENGTH_SHORT).show()); // show error message to users
+ new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(context, infoResponse, Toast.LENGTH_SHORT).show());//show error message to users
}
- @Override
- public void onResponse(Call call, Response response) throws IOException {
+ @Override public void onResponse(Call call, Response response) throws IOException {
try (ResponseBody responseBody = response.body()) {
if (!response.isSuccessful()) {
// Print error message, set success to false and throw an exception
success = false;
-// System.err.println("UPLOAD unsuccessful: " + responseBody.string());
notifyObservers(1);
-// localTrajectory.delete();
- assert responseBody != null;
String errorBody = responseBody.string();
System.err.println("UPLOAD unsuccessful: " + errorBody);
infoResponse = "Upload failed: " + errorBody;
- new Handler(Looper.getMainLooper()).post(() ->
- Toast.makeText(context, infoResponse, Toast.LENGTH_SHORT).show());
+ new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(context, infoResponse, Toast.LENGTH_SHORT).show());
throw new IOException("UPLOAD failed with code " + response);
}
@@ -343,124 +305,16 @@ public void onResponse(Call call, Response response) throws IOException {
}
// Print a confirmation of a successful POST to API
- assert responseBody != null;
System.out.println("UPLOAD SUCCESSFUL: " + responseBody.string());
// Delete local file, set success to true and notify observers
- success = localTrajectory.delete();
+ success = uploadFile.delete();
notifyObservers(1);
}
}
});
}
- /**
- * Loads download records from a JSON file and updates the downloadRecords map.
- * If the file exists, it reads the JSON content and populates the map.
- */
- private void loadDownloadRecords() {
- // Point to the app-specific Downloads folder
- File recordsDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
- File recordsFile = new File(recordsDir, "download_records.json");
-
- if (recordsFile.exists()) {
- try (BufferedReader reader = new BufferedReader(new FileReader(recordsFile))) {
- StringBuilder json = new StringBuilder();
- String line;
- while ((line = reader.readLine()) != null) {
- json.append(line);
- }
-
- JSONObject jsonObject = new JSONObject(json.toString());
- for (Iterator it = jsonObject.keys(); it.hasNext(); ) {
- String key = it.next();
- try {
- JSONObject record = jsonObject.getJSONObject(key);
- String id = record.getString("id");
- downloadRecords.put(id, record);
- } catch (Exception e) {
- System.err.println("Error loading record with key: " + key);
- e.printStackTrace();
- }
- }
-
- System.out.println("Loaded downloadRecords: " + downloadRecords);
-
- } catch (Exception e) {
- e.printStackTrace();
- }
- } else {
- System.out.println("Download_records.json not found in app-specific directory.");
- }
- }
-
- /**
- * Saves a download record to a JSON file.
- * The method creates or updates the JSON file with the provided details.
- *
- * @param startTimestamp the start timestamp of the trajectory
- * @param fileName the name of the file
- * @param id the ID of the trajectory
- * @param dateSubmitted the date the trajectory was submitted
- */
- private void saveDownloadRecord(long startTimestamp, String fileName, String id, String dateSubmitted) {
- File recordsDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
- File recordsFile = new File(recordsDir, "download_records.json");
- JSONObject jsonObject;
-
- try {
- // Ensure the directory exists
- if (recordsDir != null && !recordsDir.exists()) {
- recordsDir.mkdirs();
- }
-
- // If the file does not exist, create it
- if (!recordsFile.exists()) {
- if (recordsFile.createNewFile()) {
- jsonObject = new JSONObject();
- } else {
- System.err.println("Failed to create file: " + recordsFile.getAbsolutePath());
- return;
- }
- } else {
- // Read the existing contents
- StringBuilder jsonBuilder = new StringBuilder();
- try (BufferedReader reader = new BufferedReader(new FileReader(recordsFile))) {
- String line;
- while ((line = reader.readLine()) != null) {
- jsonBuilder.append(line);
- }
- }
- // If file is empty or invalid JSON, use a fresh JSONObject
- jsonObject = jsonBuilder.length() > 0
- ? new JSONObject(jsonBuilder.toString())
- : new JSONObject();
- }
-
- // Create the new record details
- JSONObject recordDetails = new JSONObject();
- recordDetails.put("file_name", fileName);
- recordDetails.put("startTimeStamp", startTimestamp);
- recordDetails.put("date_submitted", dateSubmitted);
- recordDetails.put("id", id);
-
- // Insert or update in the main JSON
- jsonObject.put(id, recordDetails);
-
- // Write updated JSON to file
- try (FileWriter writer = new FileWriter(recordsFile)) {
- writer.write(jsonObject.toString(4));
- writer.flush();
- }
-
- System.out.println("Download record saved successfully at: " + recordsFile.getAbsolutePath());
-
- } catch (Exception e) {
- e.printStackTrace();
- System.err.println("Error saving download record: " + e.getMessage());
- }
- }
-
/**
* Perform API request for downloading a Trajectory uploaded to the server. The trajectory is
* retrieved from a zip file, with the method accepting a position argument specifying the
@@ -468,99 +322,114 @@ private void saveDownloadRecord(long startTimestamp, String fileName, String id,
* then to a JSON string to be downloaded to the device's Downloads folder.
*
* @param position the position of the trajectory in the zip file to retrieve
- * @param id the ID of the trajectory
- * @param dateSubmitted the date the trajectory was submitted
*/
- public void downloadTrajectory(int position, String id, String dateSubmitted) {
- loadDownloadRecords(); // Load existing records from app-specific directory
+ public void downloadTrajectory(int position) {
+ if (entryList == null || position >= entryList.size()) {
+ Log.e("ServerCommunications", "Invalid position or entryList not set");
+ return;
+ }
+
+ // 获取轨迹信息
+ Map trajectory = entryList.get(position);
+ String id = trajectory.get("id");
+ String utcDateStr = trajectory.get("date_submitted");
+
+ // 添加时区转换
+ try {
+ // 解析UTC时间字符串
+ SimpleDateFormat utcFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXX");
+ utcFormat.setTimeZone(java.util.TimeZone.getTimeZone("UTC"));
+ java.util.Date utcDate = utcFormat.parse(utcDateStr);
+
+ // 转换为本地时间
+ SimpleDateFormat localFormat = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
+ localFormat.setTimeZone(java.util.TimeZone.getDefault());
+ String localDateStr = localFormat.format(utcDate);
+
+ Log.d("ServerCommunications", "UTC time: " + utcDateStr);
+ Log.d("ServerCommunications", "Local time: " + localDateStr);
+
+ // 构建云端轨迹文件路径
+ String fileName = String.format("trajectory_%s_%s.json", localDateStr, id);
+ File localDirectory = new File(context.getExternalFilesDir(null), "cloud_trajectories");
+ Log.d("ServerCommunications", "Cloud trajectories directory: " + localDirectory.getAbsolutePath());
+ File localFile = new File(localDirectory, fileName);
+
+ // 如果本地文件已存在,直接返回成功
+ if (localFile.exists()) {
+ Log.d("ServerCommunications", "Local file already exists: " + localFile.getAbsolutePath());
+ // 更新映射文件
+ updateMappingFile(id, fileName);
+ new Handler(Looper.getMainLooper()).post(() -> {
+ success = true;
+ notifyObservers(1);
+ });
+ return;
+ }
+
+ // 否则从服务器下载
+ downloadFile(id, localFile, localDirectory);
+
+ } catch (Exception e) {
+ Log.e("ServerCommunications", "Error converting timezone", e);
+ success = false;
+ notifyObservers(1);
+ }
+ }
- // Initialise OkHttp client
+ private void downloadFile(String id, File localFile, File localDirectory) {
OkHttpClient client = new OkHttpClient();
-
- // Create GET request with required header
+ String downloadUrl = downloadURL + "&id=" + id;
+
okhttp3.Request request = new okhttp3.Request.Builder()
- .url(downloadURL)
- .addHeader("accept", PROTOCOL_ACCEPT_TYPE)
- .get()
- .build();
-
- // Enqueue the GET request for asynchronous execution
+ .url(downloadUrl)
+ .addHeader("accept", PROTOCOL_ACCEPT_TYPE)
+ .get()
+ .build();
+
client.newCall(request).enqueue(new okhttp3.Callback() {
@Override
- public void onFailure(Call call, IOException e) {
- e.printStackTrace();
+ public void onFailure(@NonNull Call call, @NonNull IOException e) {
+ Log.e("ServerCommunications", "Download failed", e);
+ new Handler(Looper.getMainLooper()).post(() -> {
+ success = false;
+ notifyObservers(1);
+ });
}
@Override
- public void onResponse(Call call, Response response) throws IOException {
+ public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
try (ResponseBody responseBody = response.body()) {
- if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
-
- // Extract the nth entry from the zip
- InputStream inputStream = responseBody.byteStream();
- ZipInputStream zipInputStream = new ZipInputStream(inputStream);
-
- java.util.zip.ZipEntry zipEntry;
- int zipCount = 0;
- while ((zipEntry = zipInputStream.getNextEntry()) != null) {
- if (zipCount == position) {
- // break if zip entry position matches the desired position
- break;
- }
- zipCount++;
+ if (!response.isSuccessful()) {
+ Log.e("ServerCommunications", "Download unsuccessful: " + responseBody.string());
+ new Handler(Looper.getMainLooper()).post(() -> {
+ success = false;
+ notifyObservers(1);
+ });
+ return;
}
- // Initialise a byte array output stream
- ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
-
- // Read the zipped data and write it to the byte array output stream
- byte[] buffer = new byte[1024];
- int bytesRead;
- while ((bytesRead = zipInputStream.read(buffer)) != -1) {
- byteArrayOutputStream.write(buffer, 0, bytesRead);
+ // 确保目录存在
+ if (!localDirectory.exists()) {
+ localDirectory.mkdirs();
}
-
- // Convert the byte array to protobuf
- byte[] byteArray = byteArrayOutputStream.toByteArray();
- Traj.Trajectory receivedTrajectory = Traj.Trajectory.parseFrom(byteArray);
-
- // Inspect the size of the received trajectory
- logDataSize(receivedTrajectory);
-
- // Print a message in the console
- long startTimestamp = receivedTrajectory.getStartTimestamp();
- String fileName = "trajectory_" + dateSubmitted + ".txt";
-
- // Place the file in your app-specific "Downloads" folder
- File appSpecificDownloads = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
- if (appSpecificDownloads != null && !appSpecificDownloads.exists()) {
- appSpecificDownloads.mkdirs();
+ // 保存文件
+ try (FileOutputStream fos = new FileOutputStream(localFile)) {
+ fos.write(responseBody.bytes());
}
- File file = new File(appSpecificDownloads, fileName);
- try (FileWriter fileWriter = new FileWriter(file)) {
- String receivedTrajectoryString = JsonFormat.printer().print(receivedTrajectory);
- fileWriter.write(receivedTrajectoryString);
- fileWriter.flush();
- System.err.println("Received trajectory stored in: " + file.getAbsolutePath());
- } catch (IOException ee) {
- System.err.println("Trajectory download failed");
- } finally {
- // Close all streams and entries to release resources
- zipInputStream.closeEntry();
- byteArrayOutputStream.close();
- zipInputStream.close();
- inputStream.close();
- }
+ // 更新映射文件
+ updateMappingFile(id, localFile.getName());
- // Save the download record
- saveDownloadRecord(startTimestamp, fileName, id, dateSubmitted);
- loadDownloadRecords();
+ Log.d("ServerCommunications", "Download successful: " + localFile.getAbsolutePath());
+ new Handler(Looper.getMainLooper()).post(() -> {
+ success = true;
+ notifyObservers(1);
+ });
}
}
});
-
}
/**
@@ -620,18 +489,6 @@ private void checkNetworkStatus() {
}
}
-
- private void logDataSize(Traj.Trajectory trajectory) {
- Log.i("ServerCommunications", "IMU Data size: " + trajectory.getImuDataCount());
- Log.i("ServerCommunications", "Position Data size: " + trajectory.getPositionDataCount());
- Log.i("ServerCommunications", "Pressure Data size: " + trajectory.getPressureDataCount());
- Log.i("ServerCommunications", "Light Data size: " + trajectory.getLightDataCount());
- Log.i("ServerCommunications", "GNSS Data size: " + trajectory.getGnssDataCount());
- Log.i("ServerCommunications", "WiFi Data size: " + trajectory.getWifiDataCount());
- Log.i("ServerCommunications", "APS Data size: " + trajectory.getApsDataCount());
- Log.i("ServerCommunications", "PDR Data size: " + trajectory.getPdrDataCount());
- }
-
/**
* {@inheritDoc}
*
@@ -642,7 +499,11 @@ private void logDataSize(Traj.Trajectory trajectory) {
*/
@Override
public void registerObserver(Observer o) {
- this.observers.add(o);
+ Log.d("ServerCommunications", "Registering observer: " + o.getClass().getSimpleName());
+ if (observers == null) {
+ observers = new ArrayList<>();
+ }
+ observers.add(o);
}
/**
@@ -655,13 +516,54 @@ public void registerObserver(Observer o) {
*/
@Override
public void notifyObservers(int index) {
- for(Observer o : observers) {
- if(index == 0 && o instanceof FilesFragment) {
- o.update(new String[] {infoResponse});
+ Log.d("ServerCommunications", "Notifying observers with success: " + success);
+ if (observers != null) {
+ Log.d("ServerCommunications", "Number of observers: " + observers.size());
+ for (Observer observer : observers) {
+ if (index == 0 && observer instanceof FilesFragment) {
+ observer.update(new String[] {infoResponse});
+ } else if (index == 1) {
+ observer.update(new Boolean[] {success});
+ Log.d("ServerCommunications", "Notifying " + observer.getClass().getSimpleName() + " with success: " + success);
+ }
}
- else if (index == 1 && o instanceof MainActivity) {
- o.update(new Boolean[] {success});
+ } else {
+ Log.e("ServerCommunications", "No observers registered");
+ }
+ }
+
+ // 添加更新映射文件的方法
+ private void updateMappingFile(String cloudId, String localFileName) {
+ try {
+ File mappingFile = new File(context.getExternalFilesDir(null), "trajectory_mapping.json");
+ JSONObject mapping;
+
+ if (mappingFile.exists()) {
+ // 读取现有映射
+ StringBuilder content = new StringBuilder();
+ try (BufferedReader reader = new BufferedReader(new FileReader(mappingFile))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ content.append(line);
+ }
+ }
+ mapping = new JSONObject(content.toString());
+ } else {
+ // 创建新的映射
+ mapping = new JSONObject();
+ }
+
+ // 更新映射
+ mapping.put(cloudId, localFileName);
+
+ // 保存映射文件
+ try (FileWriter writer = new FileWriter(mappingFile)) {
+ writer.write(mapping.toString());
}
+
+ Log.d("ServerCommunications", "Updated mapping file for cloud ID: " + cloudId + " -> " + localFileName);
+ } catch (Exception e) {
+ Log.e("ServerCommunications", "Error updating mapping file", e);
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/Traj.java b/app/src/main/java/com/openpositioning/PositionMe/Traj.java
index 7925fa55..24d55196 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/Traj.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/Traj.java
@@ -578,7 +578,7 @@ private Trajectory(
return Traj.internal_static_Trajectory_descriptor;
}
- protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ protected FieldAccessorTable
internalGetFieldAccessorTable() {
return Traj.internal_static_Trajectory_fieldAccessorTable
.ensureFieldAccessorsInitialized(
@@ -1434,7 +1434,7 @@ public Builder toBuilder() {
@Override
protected Builder newBuilderForType(
- com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
+ BuilderParent parent) {
Builder builder = new Builder(parent);
return builder;
}
@@ -1450,7 +1450,7 @@ public static final class Builder extends
return Traj.internal_static_Trajectory_descriptor;
}
- protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ protected FieldAccessorTable
internalGetFieldAccessorTable() {
return Traj.internal_static_Trajectory_fieldAccessorTable
.ensureFieldAccessorsInitialized(
@@ -1463,7 +1463,7 @@ private Builder() {
}
private Builder(
- com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
+ BuilderParent parent) {
super(parent);
maybeForceBuilderInitialization();
}
@@ -4973,7 +4973,7 @@ private Pdr_Sample(
return Traj.internal_static_Pdr_Sample_descriptor;
}
- protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ protected FieldAccessorTable
internalGetFieldAccessorTable() {
return Traj.internal_static_Pdr_Sample_fieldAccessorTable
.ensureFieldAccessorsInitialized(
@@ -5180,7 +5180,7 @@ public Builder toBuilder() {
@Override
protected Builder newBuilderForType(
- com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
+ BuilderParent parent) {
Builder builder = new Builder(parent);
return builder;
}
@@ -5196,7 +5196,7 @@ public static final class Builder extends
return Traj.internal_static_Pdr_Sample_descriptor;
}
- protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ protected FieldAccessorTable
internalGetFieldAccessorTable() {
return Traj.internal_static_Pdr_Sample_fieldAccessorTable
.ensureFieldAccessorsInitialized(
@@ -5209,7 +5209,7 @@ private Builder() {
}
private Builder(
- com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
+ BuilderParent parent) {
super(parent);
maybeForceBuilderInitialization();
}
@@ -5694,7 +5694,7 @@ private Motion_Sample(
return Traj.internal_static_Motion_Sample_descriptor;
}
- protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ protected FieldAccessorTable
internalGetFieldAccessorTable() {
return Traj.internal_static_Motion_Sample_fieldAccessorTable
.ensureFieldAccessorsInitialized(
@@ -6115,7 +6115,7 @@ public Builder toBuilder() {
@Override
protected Builder newBuilderForType(
- com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
+ BuilderParent parent) {
Builder builder = new Builder(parent);
return builder;
}
@@ -6131,7 +6131,7 @@ public static final class Builder extends
return Traj.internal_static_Motion_Sample_descriptor;
}
- protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ protected FieldAccessorTable
internalGetFieldAccessorTable() {
return Traj.internal_static_Motion_Sample_fieldAccessorTable
.ensureFieldAccessorsInitialized(
@@ -6144,7 +6144,7 @@ private Builder() {
}
private Builder(
- com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
+ BuilderParent parent) {
super(parent);
maybeForceBuilderInitialization();
}
@@ -6843,7 +6843,7 @@ private Position_Sample(
return Traj.internal_static_Position_Sample_descriptor;
}
- protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ protected FieldAccessorTable
internalGetFieldAccessorTable() {
return Traj.internal_static_Position_Sample_fieldAccessorTable
.ensureFieldAccessorsInitialized(
@@ -7067,7 +7067,7 @@ public Builder toBuilder() {
@Override
protected Builder newBuilderForType(
- com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
+ BuilderParent parent) {
Builder builder = new Builder(parent);
return builder;
}
@@ -7083,7 +7083,7 @@ public static final class Builder extends
return Traj.internal_static_Position_Sample_descriptor;
}
- protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ protected FieldAccessorTable
internalGetFieldAccessorTable() {
return Traj.internal_static_Position_Sample_fieldAccessorTable
.ensureFieldAccessorsInitialized(
@@ -7096,7 +7096,7 @@ private Builder() {
}
private Builder(
- com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
+ BuilderParent parent) {
super(parent);
maybeForceBuilderInitialization();
}
@@ -7469,7 +7469,7 @@ private Pressure_Sample(
return Traj.internal_static_Pressure_Sample_descriptor;
}
- protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ protected FieldAccessorTable
internalGetFieldAccessorTable() {
return Traj.internal_static_Pressure_Sample_fieldAccessorTable
.ensureFieldAccessorsInitialized(
@@ -7647,7 +7647,7 @@ public Builder toBuilder() {
@Override
protected Builder newBuilderForType(
- com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
+ BuilderParent parent) {
Builder builder = new Builder(parent);
return builder;
}
@@ -7663,7 +7663,7 @@ public static final class Builder extends
return Traj.internal_static_Pressure_Sample_descriptor;
}
- protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ protected FieldAccessorTable
internalGetFieldAccessorTable() {
return Traj.internal_static_Pressure_Sample_fieldAccessorTable
.ensureFieldAccessorsInitialized(
@@ -7676,7 +7676,7 @@ private Builder() {
}
private Builder(
- com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
+ BuilderParent parent) {
super(parent);
maybeForceBuilderInitialization();
}
@@ -7985,7 +7985,7 @@ private Light_Sample(
return Traj.internal_static_Light_Sample_descriptor;
}
- protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ protected FieldAccessorTable
internalGetFieldAccessorTable() {
return Traj.internal_static_Light_Sample_fieldAccessorTable
.ensureFieldAccessorsInitialized(
@@ -8163,7 +8163,7 @@ public Builder toBuilder() {
@Override
protected Builder newBuilderForType(
- com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
+ BuilderParent parent) {
Builder builder = new Builder(parent);
return builder;
}
@@ -8179,7 +8179,7 @@ public static final class Builder extends
return Traj.internal_static_Light_Sample_descriptor;
}
- protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ protected FieldAccessorTable
internalGetFieldAccessorTable() {
return Traj.internal_static_Light_Sample_fieldAccessorTable
.ensureFieldAccessorsInitialized(
@@ -8192,7 +8192,7 @@ private Builder() {
}
private Builder(
- com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
+ BuilderParent parent) {
super(parent);
maybeForceBuilderInitialization();
}
@@ -8587,7 +8587,7 @@ private GNSS_Sample(
return Traj.internal_static_GNSS_Sample_descriptor;
}
- protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ protected FieldAccessorTable
internalGetFieldAccessorTable() {
return Traj.internal_static_GNSS_Sample_fieldAccessorTable
.ensureFieldAccessorsInitialized(
@@ -8926,7 +8926,7 @@ public Builder toBuilder() {
@Override
protected Builder newBuilderForType(
- com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
+ BuilderParent parent) {
Builder builder = new Builder(parent);
return builder;
}
@@ -8942,7 +8942,7 @@ public static final class Builder extends
return Traj.internal_static_GNSS_Sample_descriptor;
}
- protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ protected FieldAccessorTable
internalGetFieldAccessorTable() {
return Traj.internal_static_GNSS_Sample_fieldAccessorTable
.ensureFieldAccessorsInitialized(
@@ -8955,7 +8955,7 @@ private Builder() {
}
private Builder(
- com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
+ BuilderParent parent) {
super(parent);
maybeForceBuilderInitialization();
}
@@ -9561,7 +9561,7 @@ private WiFi_Sample(
return Traj.internal_static_WiFi_Sample_descriptor;
}
- protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ protected FieldAccessorTable
internalGetFieldAccessorTable() {
return Traj.internal_static_WiFi_Sample_fieldAccessorTable
.ensureFieldAccessorsInitialized(
@@ -9761,7 +9761,7 @@ public Builder toBuilder() {
@Override
protected Builder newBuilderForType(
- com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
+ BuilderParent parent) {
Builder builder = new Builder(parent);
return builder;
}
@@ -9777,7 +9777,7 @@ public static final class Builder extends
return Traj.internal_static_WiFi_Sample_descriptor;
}
- protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ protected FieldAccessorTable
internalGetFieldAccessorTable() {
return Traj.internal_static_WiFi_Sample_fieldAccessorTable
.ensureFieldAccessorsInitialized(
@@ -9790,7 +9790,7 @@ private Builder() {
}
private Builder(
- com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
+ BuilderParent parent) {
super(parent);
maybeForceBuilderInitialization();
}
@@ -10358,7 +10358,7 @@ private Mac_Scan(
return Traj.internal_static_Mac_Scan_descriptor;
}
- protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ protected FieldAccessorTable
internalGetFieldAccessorTable() {
return Traj.internal_static_Mac_Scan_fieldAccessorTable
.ensureFieldAccessorsInitialized(
@@ -10560,7 +10560,7 @@ public Builder toBuilder() {
@Override
protected Builder newBuilderForType(
- com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
+ BuilderParent parent) {
Builder builder = new Builder(parent);
return builder;
}
@@ -10576,7 +10576,7 @@ public static final class Builder extends
return Traj.internal_static_Mac_Scan_descriptor;
}
- protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ protected FieldAccessorTable
internalGetFieldAccessorTable() {
return Traj.internal_static_Mac_Scan_fieldAccessorTable
.ensureFieldAccessorsInitialized(
@@ -10589,7 +10589,7 @@ private Builder() {
}
private Builder(
- com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
+ BuilderParent parent) {
super(parent);
maybeForceBuilderInitialization();
}
@@ -10978,7 +10978,7 @@ private AP_Data(
return Traj.internal_static_AP_Data_descriptor;
}
- protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ protected FieldAccessorTable
internalGetFieldAccessorTable() {
return Traj.internal_static_AP_Data_fieldAccessorTable
.ensureFieldAccessorsInitialized(
@@ -11211,7 +11211,7 @@ public Builder toBuilder() {
@Override
protected Builder newBuilderForType(
- com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
+ BuilderParent parent) {
Builder builder = new Builder(parent);
return builder;
}
@@ -11227,7 +11227,7 @@ public static final class Builder extends
return Traj.internal_static_AP_Data_descriptor;
}
- protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ protected FieldAccessorTable
internalGetFieldAccessorTable() {
return Traj.internal_static_AP_Data_fieldAccessorTable
.ensureFieldAccessorsInitialized(
@@ -11240,7 +11240,7 @@ private Builder() {
}
private Builder(
- com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
+ BuilderParent parent) {
super(parent);
maybeForceBuilderInitialization();
}
@@ -11712,7 +11712,7 @@ private Sensor_Info(
return Traj.internal_static_Sensor_Info_descriptor;
}
- protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ protected FieldAccessorTable
internalGetFieldAccessorTable() {
return Traj.internal_static_Sensor_Info_fieldAccessorTable
.ensureFieldAccessorsInitialized(
@@ -12016,7 +12016,7 @@ public Builder toBuilder() {
@Override
protected Builder newBuilderForType(
- com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
+ BuilderParent parent) {
Builder builder = new Builder(parent);
return builder;
}
@@ -12032,7 +12032,7 @@ public static final class Builder extends
return Traj.internal_static_Sensor_Info_descriptor;
}
- protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ protected FieldAccessorTable
internalGetFieldAccessorTable() {
return Traj.internal_static_Sensor_Info_fieldAccessorTable
.ensureFieldAccessorsInitialized(
@@ -12045,7 +12045,7 @@ private Builder() {
}
private Builder(
- com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
+ BuilderParent parent) {
super(parent);
maybeForceBuilderInitialization();
}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/UtilFunctions.java b/app/src/main/java/com/openpositioning/PositionMe/UtilFunctions.java
similarity index 95%
rename from app/src/main/java/com/openpositioning/PositionMe/utils/UtilFunctions.java
rename to app/src/main/java/com/openpositioning/PositionMe/UtilFunctions.java
index cba92328..faf9f5f7 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/utils/UtilFunctions.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/UtilFunctions.java
@@ -1,4 +1,4 @@
-package com.openpositioning.PositionMe.utils;
+package com.openpositioning.PositionMe;
import android.content.Context;
import android.graphics.Bitmap;
@@ -8,11 +8,10 @@
import androidx.core.content.ContextCompat;
import com.google.android.gms.maps.model.LatLng;
-import com.openpositioning.PositionMe.presentation.fragment.RecordingFragment;
/**
* Class containing utility functions which can used by other classes.
- * @see RecordingFragment Currently used by RecordingFragment
+ * @see com.openpositioning.PositionMe.fragments.RecordingFragment Currently used by RecordingFragment
*/
public class UtilFunctions {
// Constant 1degree of latitiude/longitude (in m)
diff --git a/app/src/main/java/com/openpositioning/PositionMe/data/local/TrajParser.java b/app/src/main/java/com/openpositioning/PositionMe/data/local/TrajParser.java
deleted file mode 100644
index 2d2b1cbf..00000000
--- a/app/src/main/java/com/openpositioning/PositionMe/data/local/TrajParser.java
+++ /dev/null
@@ -1,256 +0,0 @@
-package com.openpositioning.PositionMe.data.local;
-
-import android.content.Context;
-import android.hardware.SensorManager;
-import android.util.Log;
-
-import com.google.android.gms.maps.model.LatLng;
-import com.google.gson.Gson;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-import com.openpositioning.PositionMe.presentation.fragment.ReplayFragment;
-import com.openpositioning.PositionMe.sensors.SensorFusion;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileReader;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-
-/**
- * Handles parsing of trajectory data stored in JSON files, combining IMU, PDR, and GNSS data
- * to reconstruct motion paths.
- *
- *
- * The **TrajParser** is primarily responsible for processing recorded trajectory data and
- * reconstructing motion information, including estimated positions, GNSS coordinates, speed, and orientation.
- * It does this by reading a JSON file containing:
- *
- *
- * IMU (Inertial Measurement Unit) data
- * PDR (Pedestrian Dead Reckoning) position data
- * GNSS (Global Navigation Satellite System) location data
- *
- *
- *
- * **Usage in Module 'PositionMe.app.main':**
- *
- *
- * **ReplayFragment** - Calls `parseTrajectoryData()` to read recorded trajectory files and process movement.
- * Stores parsed trajectory data as `ReplayPoint` objects.
- * Provides data for updating map visualizations in `ReplayFragment`.
- *
- *
- * @see ReplayFragment which uses parsed trajectory data for visualization.
- * @see SensorFusion for motion processing and sensor integration.
- * @see com.openpositioning.PositionMe.presentation.fragment.ReplayFragment for implementation details.
- *
- * @author Shu Gu
- * @author Lin Cheng
- */
-public class TrajParser {
-
- private static final String TAG = "TrajParser";
-
- /**
- * Represents a single replay point containing estimated PDR position, GNSS location,
- * orientation, speed, and timestamp.
- */
- public static class ReplayPoint {
- public LatLng pdrLocation; // PDR-derived location estimate
- public LatLng gnssLocation; // GNSS location (may be null if unavailable)
- public float orientation; // Orientation in degrees
- public float speed; // Speed in meters per second
- public long timestamp; // Relative timestamp
-
- /**
- * Constructs a ReplayPoint.
- *
- * @param pdrLocation The pedestrian dead reckoning (PDR) location.
- * @param gnssLocation The GNSS location, or null if unavailable.
- * @param orientation The orientation angle in degrees.
- * @param speed The speed in meters per second.
- * @param timestamp The timestamp associated with this point.
- */
- public ReplayPoint(LatLng pdrLocation, LatLng gnssLocation, float orientation, float speed, long timestamp) {
- this.pdrLocation = pdrLocation;
- this.gnssLocation = gnssLocation;
- this.orientation = orientation;
- this.speed = speed;
- this.timestamp = timestamp;
- }
- }
-
- /** Represents an IMU (Inertial Measurement Unit) data record used for orientation calculations. */
- private static class ImuRecord {
- public long relativeTimestamp;
- public float accX, accY, accZ; // Accelerometer values
- public float gyrX, gyrY, gyrZ; // Gyroscope values
- public float rotationVectorX, rotationVectorY, rotationVectorZ, rotationVectorW; // Rotation quaternion
- }
-
- /** Represents a Pedestrian Dead Reckoning (PDR) data record storing position shifts over time. */
- private static class PdrRecord {
- public long relativeTimestamp;
- public float x, y; // Position relative to the starting point
- }
-
- /** Represents a GNSS (Global Navigation Satellite System) data record with latitude/longitude. */
- private static class GnssRecord {
- public long relativeTimestamp;
- public double latitude, longitude; // GNSS coordinates
- }
-
- /**
- * Parses trajectory data from a JSON file and reconstructs a list of replay points.
- *
- *
- * This method processes a trajectory log file, extracting IMU, PDR, and GNSS records,
- * and uses them to generate **ReplayPoint** objects. Each point contains:
- *
- *
- * Estimated PDR-based position.
- * GNSS location (if available).
- * Computed orientation using rotation vectors.
- * Speed estimation based on movement data.
- *
- *
- * @param filePath Path to the JSON file containing trajectory data.
- * @param context Android application context (used for sensor processing).
- * @param originLat Latitude of the reference origin.
- * @param originLng Longitude of the reference origin.
- * @return A list of parsed {@link ReplayPoint} objects.
- */
- public static List parseTrajectoryData(String filePath, Context context,
- double originLat, double originLng) {
- List result = new ArrayList<>();
-
- try {
- File file = new File(filePath);
- if (!file.exists()) {
- Log.e(TAG, "File does NOT exist: " + filePath);
- return result;
- }
- if (!file.canRead()) {
- Log.e(TAG, "File is NOT readable: " + filePath);
- return result;
- }
-
- BufferedReader br = new BufferedReader(new FileReader(file));
- JsonObject root = new JsonParser().parse(br).getAsJsonObject();
- br.close();
-
- Log.i(TAG, "Successfully read trajectory file: " + filePath);
-
- long startTimestamp = root.has("startTimestamp") ? root.get("startTimestamp").getAsLong() : 0;
-
- List imuList = parseImuData(root.getAsJsonArray("imuData"));
- List pdrList = parsePdrData(root.getAsJsonArray("pdrData"));
- List gnssList = parseGnssData(root.getAsJsonArray("gnssData"));
-
- Log.i(TAG, "Parsed data - IMU: " + imuList.size() + " records, PDR: "
- + pdrList.size() + " records, GNSS: " + gnssList.size() + " records");
-
- for (int i = 0; i < pdrList.size(); i++) {
- PdrRecord pdr = pdrList.get(i);
-
- ImuRecord closestImu = findClosestImuRecord(imuList, pdr.relativeTimestamp);
- float orientationDeg = closestImu != null ? computeOrientationFromRotationVector(
- closestImu.rotationVectorX,
- closestImu.rotationVectorY,
- closestImu.rotationVectorZ,
- closestImu.rotationVectorW,
- context
- ) : 0f;
-
- float speed = 0f;
- if (i > 0) {
- PdrRecord prev = pdrList.get(i - 1);
- double dt = (pdr.relativeTimestamp - prev.relativeTimestamp) / 1000.0;
- double dx = pdr.x - prev.x;
- double dy = pdr.y - prev.y;
- double distance = Math.sqrt(dx * dx + dy * dy);
- if (dt > 0) speed = (float) (distance / dt);
- }
-
-
- double lat = originLat + pdr.y * 1E-5;
- double lng = originLng + pdr.x * 1E-5;
- LatLng pdrLocation = new LatLng(lat, lng);
-
- GnssRecord closestGnss = findClosestGnssRecord(gnssList, pdr.relativeTimestamp);
- LatLng gnssLocation = closestGnss != null ?
- new LatLng(closestGnss.latitude, closestGnss.longitude) : null;
-
- result.add(new ReplayPoint(pdrLocation, gnssLocation, orientationDeg,
- 0f, pdr.relativeTimestamp));
- }
-
- Collections.sort(result, Comparator.comparingLong(rp -> rp.timestamp));
-
- Log.i(TAG, "Final ReplayPoints count: " + result.size());
-
- } catch (Exception e) {
- Log.e(TAG, "Error parsing trajectory file!", e);
- }
-
- return result;
- }
-/** Parses IMU data from JSON. */
-private static List parseImuData(JsonArray imuArray) {
- List imuList = new ArrayList<>();
- if (imuArray == null) return imuList;
- Gson gson = new Gson();
- for (int i = 0; i < imuArray.size(); i++) {
- ImuRecord record = gson.fromJson(imuArray.get(i), ImuRecord.class);
- imuList.add(record);
- }
- return imuList;
-}/** Parses PDR data from JSON. */
-private static List parsePdrData(JsonArray pdrArray) {
- List pdrList = new ArrayList<>();
- if (pdrArray == null) return pdrList;
- Gson gson = new Gson();
- for (int i = 0; i < pdrArray.size(); i++) {
- PdrRecord record = gson.fromJson(pdrArray.get(i), PdrRecord.class);
- pdrList.add(record);
- }
- return pdrList;
-}/** Parses GNSS data from JSON. */
-private static List parseGnssData(JsonArray gnssArray) {
- List gnssList = new ArrayList<>();
- if (gnssArray == null) return gnssList;
- Gson gson = new Gson();
- for (int i = 0; i < gnssArray.size(); i++) {
- GnssRecord record = gson.fromJson(gnssArray.get(i), GnssRecord.class);
- gnssList.add(record);
- }
- return gnssList;
-}/** Finds the closest IMU record to the given timestamp. */
-private static ImuRecord findClosestImuRecord(List imuList, long targetTimestamp) {
- return imuList.stream().min(Comparator.comparingLong(imu -> Math.abs(imu.relativeTimestamp - targetTimestamp)))
- .orElse(null);
-
-}/** Finds the closest GNSS record to the given timestamp. */
-private static GnssRecord findClosestGnssRecord(List gnssList, long targetTimestamp) {
- return gnssList.stream().min(Comparator.comparingLong(gnss -> Math.abs(gnss.relativeTimestamp - targetTimestamp)))
- .orElse(null);
-
-}/** Computes the orientation from a rotation vector. */
-private static float computeOrientationFromRotationVector(float rx, float ry, float rz, float rw, Context context) {
- float[] rotationVector = new float[]{rx, ry, rz, rw};
- float[] rotationMatrix = new float[9];
- float[] orientationAngles = new float[3];
-
- SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
- SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVector);
- SensorManager.getOrientation(rotationMatrix, orientationAngles);
-
- float azimuthDeg = (float) Math.toDegrees(orientationAngles[0]);
- return azimuthDeg < 0 ? azimuthDeg + 360.0f : azimuthDeg;
-}
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/openpositioning/PositionMe/fragments/CorrectionFragment.java b/app/src/main/java/com/openpositioning/PositionMe/fragments/CorrectionFragment.java
new file mode 100644
index 00000000..c91e380c
--- /dev/null
+++ b/app/src/main/java/com/openpositioning/PositionMe/fragments/CorrectionFragment.java
@@ -0,0 +1,230 @@
+package com.openpositioning.PositionMe.fragments;
+
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.fragment.app.Fragment;
+import androidx.navigation.NavDirections;
+import androidx.navigation.Navigation;
+
+import com.google.android.gms.maps.CameraUpdateFactory;
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.OnMapReadyCallback;
+import com.google.android.gms.maps.SupportMapFragment;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.MarkerOptions;
+import com.openpositioning.PositionMe.PathView;
+import com.openpositioning.PositionMe.R;
+import com.openpositioning.PositionMe.sensors.SensorFusion;
+
+/**
+ * A simple {@link Fragment} subclass. Corrections Fragment is displayed after a recording session
+ * is finished to enable manual adjustments to the PDR. The adjustments are not saved as of now.
+ *
+ * @see RecordingFragment the preceeding fragment in the nav graph.
+ * @see HomeFragment the next fragment in the nav graph.
+ *
+ *
+ * @author Michal Dvorak
+ * @author Mate Stodulka
+ * @author Virginia Cangelosi
+ */
+public class CorrectionFragment extends Fragment {
+
+ //Map variable to assign to map fragment
+ public GoogleMap mMap;
+ //Button to go to next fragment and save the corrections
+ private Button button;
+ //Singleton SensorFusion class which stores data from all sensors
+ private SensorFusion sensorFusion = SensorFusion.getInstance();
+ //TextView to display user instructions
+ private TextView averageStepLengthText;
+ //Text Input to edit step length
+ private EditText stepLengthInput;
+ //Average step length obtained from SensorFusion class
+ private float averageStepLength;
+ //User entered step length
+ private float newStepLength;
+ //OnKey is called twice so ensure only the second run updates the previous value for the scaling
+ private int secondPass = 0;
+ //Raw text entered by user
+ private CharSequence changedText;
+ //Scaling ratio based on size of trajectory
+ private static float scalingRatio = 0f;
+ //Initial location of PDR
+ private static LatLng start;
+ //Path view on screen
+ private PathView pathView;
+
+ /**
+ * Public Constructor for the class.
+ * Left empty as not required
+ */
+ public CorrectionFragment() {
+ // Required empty public constructor
+ }
+
+ /**
+ * {@inheritDoc}
+ * Loads the starting position set in {@link StartLocationFragment}, and displays a map fragment.
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ // Inflate the layout for this fragment
+ View rootView = inflater.inflate(R.layout.fragment_correction, container, false);
+
+ // Inflate the layout for this fragment
+ ((AppCompatActivity)getActivity()).getSupportActionBar().hide();
+
+ //Send trajectory data to the cloud
+ sensorFusion.sendTrajectoryToCloud();
+
+ //Obtain start position set in the startLocation fragment
+ float[] startPosition = sensorFusion.getGNSSLatitude(true);
+
+ // Initialize map fragment
+ SupportMapFragment supportMapFragment=(SupportMapFragment)
+ getChildFragmentManager().findFragmentById(R.id.map);
+
+ // Asynchronous map which can be configured
+ supportMapFragment.getMapAsync(new OnMapReadyCallback() {
+ /**
+ * {@inheritDoc}
+ * Controls to allow scrolling, tilting, rotating and a compass view of the
+ * map are enabled. A marker is added to the map with the start position and the PDR
+ * trajectory is scaled before being overlaid over the map fragment in
+ * CorrectionFragment.onViewCreated.
+ *
+ * @param map Google map to be configured
+ */
+ @Override
+ public void onMapReady(GoogleMap map) {
+ mMap = map;
+ mMap.setMapType(GoogleMap.MAP_TYPE_HYBRID);
+ mMap.getUiSettings().setCompassEnabled(true);
+ mMap.getUiSettings().setTiltGesturesEnabled(true);
+ mMap.getUiSettings().setRotateGesturesEnabled(true);
+ mMap.getUiSettings().setScrollGesturesEnabled(true);
+
+ // Add a marker at the start position and move the camera
+ start = new LatLng(startPosition[0], startPosition[1]);
+ mMap.addMarker(new MarkerOptions().position(start).title("Start Position"));
+ System.out.println("onMapReady scaling ratio: " + scalingRatio);
+ // Calculate zoom of google maps based on the scaling ration from PathView
+ double zoom = Math.log(156543.03392f * Math.cos(startPosition[0] * Math.PI / 180)
+ * scalingRatio) / Math.log(2);
+ System.out.println("onMapReady zoom: " + zoom);
+ //Center the camera
+ mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(start, (float) zoom));
+ }
+ });
+
+ return rootView;
+ }
+
+ /**
+ * {@inheritDoc}.
+ * Button onClick listener enabled to detect when to go to next fragment and show the action bar.
+ * Load and display average step length from PDR.
+ */
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ //Instantiate text view to show average step length
+ this.averageStepLengthText = (TextView) getView().findViewById(R.id.averageStepView);
+ //Instantiate input text view to edit average step length
+ this.stepLengthInput = (EditText) getView().findViewById(R.id.inputStepLength);
+ //Instantiate path view for drawing trajectory
+ this.pathView = (PathView) getView().findViewById(R.id.pathView1);
+ //obtain average step length from SensorFusion class
+ averageStepLength = sensorFusion.passAverageStepLength();
+ //Display average step count on UI
+ averageStepLengthText.setText(getActivity().getResources().getString(R.string.averageStepLgn) + ": " + String.format("%.2f", averageStepLength));
+ //Check for enter to be pressed when user inputs new step length
+ this.stepLengthInput.setOnKeyListener(new View.OnKeyListener() {
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ //Check if enter key has been pressed
+ if(keyCode == KeyEvent.KEYCODE_ENTER){
+ //Convert entered string to a float
+ newStepLength = Float.parseFloat(changedText.toString());
+ //Rescale the path and call function to redraw
+ //scalingRatio = newStepLength/averageStepLength;
+ sensorFusion.redrawPath(newStepLength/averageStepLength);
+ //Show user new average step value
+ averageStepLengthText.setText(getActivity().getResources().
+ getString(R.string.averageStepLgn) + ": " + String.format("%.2f", newStepLength));
+ //redraw the path
+ pathView.invalidate();
+ //OnKew is called twice (once on press and release of button so the previous
+ // step count is updated only the second time)
+ secondPass++;
+ if(secondPass == 2) {
+ averageStepLength = newStepLength;
+ secondPass = 0;
+ }
+ }
+
+ return false;
+ }
+ });
+
+ //Detect changes in the text editor. Call all default methods and store final string
+ this.stepLengthInput.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ //store string when user has finished changing the text
+ changedText = s;
+ }
+ });
+
+ // Add button to navigate back to home screen.
+ this.button = (Button) getView().findViewById(R.id.correction_done);
+ this.button.setOnClickListener(new View.OnClickListener() {
+ /**
+ * {@inheritDoc}
+ * When button clicked the {@link HomeFragment} is loaded and the action bar is
+ * returned.
+ */
+ @Override
+ public void onClick(View view) {
+ NavDirections action = CorrectionFragmentDirections.actionCorrectionFragmentToHomeFragment();
+ Navigation.findNavController(view).navigate(action);
+ //Show action bar
+ ((AppCompatActivity)getActivity()).getSupportActionBar().show();
+ }
+ });
+ }
+
+ /**
+ * Set the scaling ration for the map fragments.
+ *
+ * @param scalingRatio float ratio for scaling zoom on Maps.
+ */
+ public void setScalingRatio(float scalingRatio) {
+ this.scalingRatio = scalingRatio;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/openpositioning/PositionMe/fragments/FilesFragment.java b/app/src/main/java/com/openpositioning/PositionMe/fragments/FilesFragment.java
new file mode 100644
index 00000000..66922cd2
--- /dev/null
+++ b/app/src/main/java/com/openpositioning/PositionMe/fragments/FilesFragment.java
@@ -0,0 +1,425 @@
+package com.openpositioning.PositionMe.fragments;
+
+import android.app.AlertDialog;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.cardview.widget.CardView;
+import androidx.fragment.app.Fragment;
+import androidx.navigation.NavDirections;
+import androidx.navigation.Navigation;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.openpositioning.PositionMe.R;
+import com.openpositioning.PositionMe.ServerCommunications;
+import com.openpositioning.PositionMe.sensors.Observer;
+import com.openpositioning.PositionMe.viewitems.DownloadClickListener;
+import com.openpositioning.PositionMe.viewitems.TrajDownloadListAdapter;
+import com.openpositioning.PositionMe.viewitems.TrajDownloadViewHolder;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.text.SimpleDateFormat;
+
+/**
+ * A simple {@link Fragment} subclass. The files fragments displays a list of trajectories already
+ * uploaded with some metadata, and enabled re-downloading them to the device's local storage.
+ *
+ * @see HomeFragment the connected fragment in the nav graph.
+ * @see UploadFragment sub-menu for uploading recordings that failed during recording.
+ * @see com.openpositioning.PositionMe.Traj the data structure sent and received.
+ * @see ServerCommunications the class handling communication with the server.
+ *
+ * @author Mate Stodulka
+ */
+public class FilesFragment extends Fragment implements Observer {
+
+ // UI elements
+ private RecyclerView filesList;
+ private TrajDownloadListAdapter listAdapter;
+ private CardView uploadCard;
+ private View rootView; // 添加这个变量来存储根视图
+
+ // Class handling HTTP communication
+ private ServerCommunications serverCommunications;
+
+ // 添加一个 Set 来记录已下载的文件 ID
+ private Set downloadedFiles = new HashSet<>();
+
+ // 添加一个变量来跟踪当前下载的位置
+ private int currentDownloadPosition = -1;
+
+ // 添加 entryList 作为类的成员变量
+ private List> entryList = new ArrayList<>();
+
+ private Map cloudToLocalFileMap = new HashMap<>();
+
+ /**
+ * Default public constructor, empty.
+ */
+ public FilesFragment() {
+ // Required empty public constructor
+ }
+
+ /**
+ * {@inheritDoc}
+ * Initialise the server communication class and register the FilesFragment as an Observer to
+ * receive the async http responses.
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ serverCommunications = new ServerCommunications(getActivity());
+ serverCommunications.registerObserver(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ * Sets the title in the action bar.
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ // Inflate the layout for this fragment
+ View view = inflater.inflate(R.layout.fragment_files, container, false);
+ getActivity().setTitle("Trajectory recordings");
+ return view;
+ }
+
+ /**
+ * {@inheritDoc}
+ * Initialises UI elements, including a navigation card to the {@link UploadFragment} and a
+ * RecyclerView displaying online trajectories.
+ *
+ * @see TrajDownloadViewHolder the View Holder for the list.
+ * @see TrajDownloadListAdapter the list adapter for displaying the recycler view.
+ * @see R.layout#item_trajectorycard_view the elements in the list.
+ */
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ // Get recyclerview
+ filesList = view.findViewById(R.id.filesList);
+ // Get clickable card view
+ uploadCard = view.findViewById(R.id.uploadCard);
+ uploadCard.setOnClickListener(new View.OnClickListener() {
+ /**
+ * {@inheritDoc}
+ * Navigates to {@link UploadFragment}.
+ */
+ @Override
+ public void onClick(View view) {
+ NavDirections action = FilesFragmentDirections.actionFilesFragmentToUploadFragment();
+ Navigation.findNavController(view).navigate(action);
+ }
+ });
+
+ // Request list of uploaded trajectories from the server.
+ serverCommunications.sendInfoRequest();
+ }
+
+ /**
+ * {@inheritDoc}
+ * Called by {@link ServerCommunications} when the response to the HTTP info request is received.
+ *
+ * @param singletonStringList a single string wrapped in an object array containing the http
+ * response from the server.
+ */
+ @Override
+ public void update(Object[] data) {
+ if (data[0] instanceof Boolean) {
+ boolean success = (Boolean) data[0];
+ if (success) {
+ Log.d("FilesFragment", "Download success notification received");
+ if (getCurrentDownloadPosition() != -1) {
+ String id = entryList.get(getCurrentDownloadPosition()).get("id");
+ downloadedFiles.add(id);
+ Log.d("FilesFragment", "Current download position: " + getCurrentDownloadPosition());
+ Log.d("FilesFragment", "Updating UI for trajectory ID: " + id);
+
+ // 更新 UI
+ requireActivity().runOnUiThread(() -> {
+ if (filesList != null && filesList.getAdapter() != null) {
+ filesList.getAdapter().notifyItemChanged(getCurrentDownloadPosition());
+ Log.d("FilesFragment", "Updated button visibility");
+ }
+ });
+ }
+ } else {
+ // 下载失败时显示提示
+ requireActivity().runOnUiThread(() -> {
+ Toast.makeText(getContext(), "Downloading ...", Toast.LENGTH_SHORT).show();
+ });
+ }
+ } else if (data[0] instanceof String) {
+ String infoString = (String) data[0];
+ Log.d("FilesFragment", "Received info string: " + (infoString != null ? infoString.substring(0, Math.min(100, infoString.length())) : "null"));
+ if(infoString != null && !infoString.isEmpty()) {
+ this.entryList = processInfoResponse(infoString);
+ new Handler(Looper.getMainLooper()).post(() -> {
+ updateView(this.entryList);
+ });
+ }
+ }
+ }
+
+ /**
+ * Parses the info response string from the HTTP communication.
+ * Process the data using the Json library and return the matching Java data structure as a
+ * List of Maps of \. Throws a JSONException if the data is not valid.
+ *
+ * @param infoString HTTP info request response as a single string
+ * @return List of Maps of String to String containing ID, owner ID, and date.
+ */
+ private List> processInfoResponse(String infoString) {
+ // Initialise empty list
+ List> entryList = new ArrayList<>();
+ try {
+ // Attempt to decode using known JSON pattern
+ JSONArray jsonArray = new JSONArray(infoString);
+ for (int i = 0; i < jsonArray.length(); i++) {
+ JSONObject trajectoryEntry = jsonArray.getJSONObject(i);
+ Map entryMap = new HashMap<>();
+ entryMap.put("owner_id", String.valueOf(trajectoryEntry.get("owner_id")));
+ entryMap.put("date_submitted", (String) trajectoryEntry.get("date_submitted"));
+ entryMap.put("id", String.valueOf(trajectoryEntry.get("id")));
+ // Add decoded map to list of entries
+ entryList.add(entryMap);
+ }
+ } catch (JSONException e) {
+ System.err.println("JSON reading failed");
+ e.printStackTrace();
+ }
+ // Sort the list by the ID fields of the maps
+ entryList.sort(Comparator.comparing(m -> Integer.parseInt(m.get("id")), Comparator.nullsLast(Comparator.naturalOrder())));
+ return entryList;
+ }
+
+ /**
+ * Update the RecyclerView in the FilesFragment with new data.
+ * Must be called from a UI thread. Initialises a new Layout Manager, and passes it to the
+ * RecyclerView. Initialises a {@link TrajDownloadListAdapter} with the input array and setting
+ * up a listener so that trajectories are downloaded when clicked, and a pop-up message is
+ * displayed to notify the user.
+ *
+ * @param newEntryList List of Maps of String to String containing metadata about the uploaded
+ * trajectories (ID, owner ID, date).
+ */
+ private void updateView(List> newEntryList) {
+ // 更新类的成员变量
+ this.entryList = newEntryList;
+
+ // Initialise RecyclerView with Manager and Adapter
+ LinearLayoutManager manager = new LinearLayoutManager(getActivity());
+ filesList.setLayoutManager(manager);
+ filesList.setHasFixedSize(true);
+ listAdapter = new TrajDownloadListAdapter(getActivity(), entryList, new DownloadClickListener() {
+ @Override
+ public void onPositionClicked(int position) {
+ Log.d("FilesFragment", "Download clicked for position: " + position);
+ currentDownloadPosition = position;
+
+ // 获取轨迹信息
+ Map trajectory = entryList.get(position);
+ Log.d("FilesFragment", "Downloading trajectory with ID: " + trajectory.get("id"));
+
+ // 设置 entryList
+ serverCommunications.setEntryList(entryList);
+
+ // 显示下载中对话框
+ AlertDialog downloadingDialog = new AlertDialog.Builder(getContext())
+ .setTitle("Downloading...")
+ .setMessage("Please wait...")
+ .setCancelable(false)
+ .show();
+
+ // 开始下载
+ Log.d("FilesFragment", "Starting download...");
+ serverCommunications.downloadTrajectory(position);
+
+ // 3秒后关闭对话框
+ new Handler().postDelayed(() -> {
+ Log.d("FilesFragment", "Closing download dialog");
+ downloadingDialog.dismiss();
+ }, 3000);
+ }
+
+ @Override
+ public void onReplayClicked(int position) {
+ try {
+ // 在location_logs目录中查找所有轨迹文件
+ File directory = new File(getActivity().getExternalFilesDir(null), "location_logs");
+ File[] allFiles = directory.listFiles((dir, name) ->
+ name.startsWith("location_log_local_") && name.endsWith(".json")
+ );
+
+ if (allFiles != null && allFiles.length > 0) {
+ // 找到最新的文件
+ File latestFile = allFiles[0];
+ for (File file : allFiles) {
+ if (file.lastModified() > latestFile.lastModified()) {
+ latestFile = file;
+ }
+ }
+
+ Log.d("FilesFragment", "Using latest file: " + latestFile.getAbsolutePath());
+ Log.d("FilesFragment", "File last modified: " + new java.util.Date(latestFile.lastModified()));
+
+ // 如果文件修改时间在最近5分钟内,直接使用该文件
+ if ((System.currentTimeMillis() - latestFile.lastModified()) < 5 * 60 * 1000) {
+ Log.d("FilesFragment", "Using recently modified file");
+ NavDirections action = FilesFragmentDirections.actionFilesFragmentToReplayFragment(latestFile.getAbsolutePath());
+ Navigation.findNavController(requireView()).navigate(action);
+ return;
+ }
+
+ // 如果不是最近的文件,则按照原来的逻辑查找匹配的文件
+ Map trajectory = entryList.get(position);
+ String utcDateStr = trajectory.get("date_submitted");
+
+ // 解析UTC时间字符串
+ SimpleDateFormat utcFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXX");
+ utcFormat.setTimeZone(java.util.TimeZone.getTimeZone("UTC"));
+ java.util.Date utcDate = utcFormat.parse(utcDateStr);
+
+ // 转换为本地时间
+ SimpleDateFormat localFormat = new SimpleDateFormat("yyyy-MM-dd_HH-mm");
+ localFormat.setTimeZone(java.util.TimeZone.getDefault());
+ String minuteTimestamp = localFormat.format(utcDate); // 格式:YYYY-MM-DD_HH-mm
+
+ Log.d("FilesFragment", "Looking for local file with timestamp: " + minuteTimestamp);
+
+ // 查找匹配的文件
+ File[] matchingFiles = directory.listFiles((dir, name) -> {
+ try {
+ if (!name.startsWith("location_log_local_") || !name.endsWith(".json")) {
+ return false;
+ }
+
+ String timestampPart = name.substring("location_log_local_".length(), name.length() - 5);
+ String[] parts = timestampPart.split("[-_]");
+
+ if (parts.length < 5) {
+ return false;
+ }
+
+ String fileTimestamp = parts[0] + "-" + parts[1] + "-" + parts[2] + "_" +
+ parts[3] + "-" + parts[4];
+
+ Log.d("FilesFragment", "Comparing timestamps - File: " + fileTimestamp + ", Target: " + minuteTimestamp);
+ return fileTimestamp.equals(minuteTimestamp);
+ } catch (Exception e) {
+ return false;
+ }
+ });
+
+ if (matchingFiles != null && matchingFiles.length > 0) {
+ File matchingFile = matchingFiles[0];
+ for (File file : matchingFiles) {
+ if (file.lastModified() > matchingFile.lastModified()) {
+ matchingFile = file;
+ }
+ }
+
+ Log.d("FilesFragment", "Found matching file: " + matchingFile.getAbsolutePath());
+ NavDirections action = FilesFragmentDirections.actionFilesFragmentToReplayFragment(matchingFile.getAbsolutePath());
+ Navigation.findNavController(requireView()).navigate(action);
+ return;
+ }
+
+ // 如果没有找到匹配的文件,使用最新的文件
+ Log.d("FilesFragment", "No matching file found, using latest file");
+ NavDirections action = FilesFragmentDirections.actionFilesFragmentToReplayFragment(latestFile.getAbsolutePath());
+ Navigation.findNavController(requireView()).navigate(action);
+ } else {
+ Log.e("FilesFragment", "No local files found");
+ Toast.makeText(getContext(), "未找到本地轨迹文件", Toast.LENGTH_SHORT).show();
+ }
+ } catch (Exception e) {
+ Log.e("FilesFragment", "Error in onReplayClicked", e);
+ Toast.makeText(getContext(), "回放失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ }
+ }) {
+ @Override
+ public void onBindViewHolder(@NonNull TrajDownloadViewHolder holder, int position) {
+ super.onBindViewHolder(holder, position);
+
+ // 恢复下载状态
+ String id = entryList.get(position).get("id");
+ if (downloadedFiles.contains(id)) {
+ holder.downloadButton.setVisibility(View.GONE);
+ holder.replayButton.setVisibility(View.VISIBLE);
+ }
+ }
+ };
+
+ filesList.setAdapter(listAdapter);
+ }
+
+ // 修改 getTrajectoryFilePath 方法,添加参数控制是否显示提示
+ private String getTrajectoryFilePath(int position, List> entryList, boolean showToast) {
+ Map trajectory = entryList.get(position);
+ String id = trajectory.get("id");
+ String date = trajectory.get("date_submitted").split("\\.")[0].replace(":", "-");
+
+ // 使用和下载时相同的文件名格式
+ String fileName = String.format("location_log_%s_%s.json", date, id);
+
+ File directory = new File(getActivity().getExternalFilesDir(null), "location_logs");
+ File file = new File(directory, fileName);
+
+ // 只在需要时显示提示
+ if (!file.exists() && showToast) {
+ Log.e("FilesFragment", "Trajectory file not found: " + file.getAbsolutePath());
+ Toast.makeText(getContext(), "请先下载轨迹文件", Toast.LENGTH_SHORT).show();
+ }
+
+ return file.getAbsolutePath();
+ }
+
+ private int getCurrentDownloadPosition() {
+ return currentDownloadPosition;
+ }
+
+ // 添加一个辅助方法来解析时间戳
+ private long parseTimestamp(String timestamp) {
+ try {
+ // 格式:YYYY-MM-DD_HH-mm
+ String[] parts = timestamp.split("[-_]");
+ if (parts.length != 5) {
+ return 0;
+ }
+
+ int year = Integer.parseInt(parts[0]);
+ int month = Integer.parseInt(parts[1]);
+ int day = Integer.parseInt(parts[2]);
+ int hour = Integer.parseInt(parts[3]);
+ int minute = Integer.parseInt(parts[4]);
+
+ return (long) year * 100000000 + month * 1000000 + day * 10000 + hour * 100 + minute;
+ } catch (Exception e) {
+ Log.e("FilesFragment", "Error parsing timestamp: " + timestamp, e);
+ return 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/openpositioning/PositionMe/fragments/HomeFragment.java b/app/src/main/java/com/openpositioning/PositionMe/fragments/HomeFragment.java
new file mode 100644
index 00000000..8443c738
--- /dev/null
+++ b/app/src/main/java/com/openpositioning/PositionMe/fragments/HomeFragment.java
@@ -0,0 +1,107 @@
+package com.openpositioning.PositionMe.fragments;
+
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.navigation.NavDirections;
+import androidx.navigation.Navigation;
+import androidx.preference.PreferenceManager;
+
+import com.openpositioning.PositionMe.R;
+import com.openpositioning.PositionMe.sensors.SensorFusion;
+
+/**
+ * Home Fragment displays the main buttons to navigate through the app.
+ * The fragment has 4 buttons to:
+ * 1) Start the recording process
+ * 2) Navigate to the sensor information screen to have more detail
+ * 3) Navigate to the measurements screen to check values in real time
+ * 4) Navigate to the files page to upload trajectories and download from the cloud.
+ *
+ * @see FilesFragment The Files Fragment
+ * @see InfoFragment Sensor information Fragment
+ * @see MeasurementsFragment The measurements Fragment
+ * @see StartLocationFragment The First fragment to start recording
+ *
+ * @author Michal Dvorak, Virginia Cangelosi
+ */
+public class HomeFragment extends Fragment {
+
+ private Button startStopButton;
+ private Button sensorInfoButton;
+ private Button measurementButton;
+ private Button filesButton;
+ private SensorFusion sensorFusion;
+ private static final String TAG = "HomeFragment";
+
+ /**
+ * Default empty constructor, unused.
+ */
+ public HomeFragment() {
+ // Required empty public constructor
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_home, container, false);
+ }
+
+ /**
+ * {@inheritDoc}
+ * Initialise UI elements and set onClick actions for the buttons.
+ */
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ sensorFusion = SensorFusion.getInstance();
+
+ startStopButton = view.findViewById(R.id.startStopButton);
+ sensorInfoButton = view.findViewById(R.id.sensorInfoButton);
+ measurementButton = view.findViewById(R.id.measurementButton);
+ filesButton = view.findViewById(R.id.filesButton);
+
+ startStopButton.setOnClickListener(v -> {
+ NavDirections action = HomeFragmentDirections.actionHomeFragmentToStartLocationFragment();
+ Navigation.findNavController(v).navigate(action);
+ });
+
+ sensorInfoButton.setOnClickListener(v -> {
+ NavDirections action = HomeFragmentDirections.actionHomeFragmentToInfoFragment();
+ Navigation.findNavController(v).navigate(action);
+ });
+
+ measurementButton.setOnClickListener(v -> {
+ NavDirections action = HomeFragmentDirections.actionHomeFragmentToMeasurementsFragment();
+ Navigation.findNavController(v).navigate(action);
+ });
+
+ filesButton.setOnClickListener(v -> {
+ NavDirections action = HomeFragmentDirections.actionHomeFragmentToFilesFragment();
+ Navigation.findNavController(v).navigate(action);
+ });
+
+ startStopButton.setEnabled(!PreferenceManager.getDefaultSharedPreferences(getContext())
+ .getBoolean("permanentDeny", false));
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/IndoorMapFragment.java b/app/src/main/java/com/openpositioning/PositionMe/fragments/IndoorMapManager.java
similarity index 92%
rename from app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/IndoorMapFragment.java
rename to app/src/main/java/com/openpositioning/PositionMe/fragments/IndoorMapManager.java
index 48c40474..2ec21a6f 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/IndoorMapFragment.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/fragments/IndoorMapManager.java
@@ -1,19 +1,18 @@
-package com.openpositioning.PositionMe.presentation.fragment;
+package com.openpositioning.PositionMe.fragments;
import com.google.android.gms.maps.GoogleMap;
-import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.BitmapDescriptor;
-
+import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.GroundOverlay;
import com.google.android.gms.maps.model.GroundOverlayOptions;
import com.google.android.gms.maps.model.LatLngBounds;
-public class IndoorMapFragment {
+public class IndoorMapManager {
private GoogleMap mMap;
private GroundOverlay[] groundOverlays; // GroundOverlay used to store each layer
private int currentFloor = 0; // Floor by default
- public IndoorMapFragment(GoogleMap map, int floorNumber) {
+ public IndoorMapManager(GoogleMap map, int floorNumber) {
this.mMap = map; // Pass in Google Maps
this.groundOverlays = new GroundOverlay[floorNumber]; // Set the number of floors
}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/InfoFragment.java b/app/src/main/java/com/openpositioning/PositionMe/fragments/InfoFragment.java
similarity index 86%
rename from app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/InfoFragment.java
rename to app/src/main/java/com/openpositioning/PositionMe/fragments/InfoFragment.java
index f0cc78de..7a1a1870 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/InfoFragment.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/fragments/InfoFragment.java
@@ -1,4 +1,4 @@
-package com.openpositioning.PositionMe.presentation.fragment;
+package com.openpositioning.PositionMe.fragments;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -12,10 +12,9 @@
import androidx.recyclerview.widget.RecyclerView;
import com.openpositioning.PositionMe.R;
-import com.openpositioning.PositionMe.presentation.viewitems.SensorInfoViewHolder;
import com.openpositioning.PositionMe.sensors.SensorFusion;
import com.openpositioning.PositionMe.sensors.SensorInfo;
-import com.openpositioning.PositionMe.presentation.viewitems.SensorInfoListAdapter;
+import com.openpositioning.PositionMe.viewitems.SensorInfoListAdapter;
import java.util.List;
@@ -24,7 +23,7 @@
* collection devices with relevant information about their capabilities.
*
* @see HomeFragment the previous fragment in the nav graph.
- * @see com.openpositioning.PositionMe.sensors.SensorFusion the class containing all sensors.
+ * @see SensorFusion the class containing all sensors.
* @see SensorInfo the class used for each sensor instance's metadata
*
* @author Mate Stodulka
@@ -71,8 +70,8 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container,
* {@link SensorInfoListAdapter}.
*
* @see SensorInfoListAdapter List adapter for the Sensor Info Recycler View.
- * @see SensorInfoViewHolder View holder for the Sensor Infor RV.
- * @see com.openpositioning.PositionMe.R.layout#item_sensorinfo_card_view
+ * @see com.openpositioning.PositionMe.viewitems.SensorInfoViewHolder View holder for the Sensor Infor RV.
+ * @see R.layout#item_sensorinfo_card_view
*/
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
diff --git a/app/src/main/java/com/openpositioning/PositionMe/fragments/MeasurementsFragment.java b/app/src/main/java/com/openpositioning/PositionMe/fragments/MeasurementsFragment.java
new file mode 100644
index 00000000..fa0252a7
--- /dev/null
+++ b/app/src/main/java/com/openpositioning/PositionMe/fragments/MeasurementsFragment.java
@@ -0,0 +1,318 @@
+package com.openpositioning.PositionMe.fragments;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.cardview.widget.CardView;
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.openpositioning.PositionMe.R;
+import com.openpositioning.PositionMe.sensors.Observer;
+import com.openpositioning.PositionMe.sensors.SensorFusion;
+import com.openpositioning.PositionMe.sensors.SensorTypes;
+import com.openpositioning.PositionMe.sensors.Wifi;
+import com.openpositioning.PositionMe.viewitems.WifiListAdapter;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A simple {@link Fragment} subclass. The measurement fragment displays the set of current sensor
+ * readings. The values are refreshed periodically, but slower than their internal refresh rate.
+ * The refresh time is set by a static constant.
+ *
+ * @see HomeFragment the previous fragment in the nav graph.
+ * @see SensorFusion the source of all sensor readings.
+ *
+ * @author Mate Stodulka
+ */
+public class MeasurementsFragment extends Fragment implements Observer {
+
+ // Static constant for refresh time in milliseconds
+ private static final long REFRESH_TIME = 5000;
+
+ // Singleton Sensor Fusion class handling all sensor data
+ private SensorFusion sensorFusion;
+
+ // UI Handler
+ private Handler refreshDataHandler;
+ // UI elements
+ private ConstraintLayout sensorMeasurementList;
+ private RecyclerView wifiListView;
+ // List of string resource IDs
+ private int[] prefaces;
+ private int[] gnssPrefaces;
+ private TextView floorTextView;
+
+ /**
+ * Public default constructor, empty.
+ */
+ public MeasurementsFragment() {
+ // Required empty public constructor
+ }
+
+ /**
+ * {@inheritDoc}
+ * Obtains the singleton Sensor Fusion instance and initialises the string prefaces for display.
+ * Creates a new handler to periodically refresh data.
+ *
+ * @see SensorFusion handles all sensor data.
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Get sensor fusion instance
+ sensorFusion = SensorFusion.getInstance();
+ // Initialise string prefaces for display
+ prefaces = new int[]{R.string.x, R.string.y, R.string.z};
+ gnssPrefaces = new int[]{R.string.lati, R.string.longi};
+
+ // Create new handler to refresh the UI.
+ this.refreshDataHandler = new Handler();
+ }
+
+ /**
+ * {@inheritDoc}
+ * Sets title in the action bar to Sensor Measurements.
+ * Posts the {@link MeasurementsFragment#refreshTableTask} using the Handler.
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ // Inflate the layout for this fragment
+ View rootView = inflater.inflate(R.layout.fragment_measurements, container, false);
+ getActivity().setTitle("Sensor Measurements");
+ this.refreshDataHandler.post(refreshTableTask);
+ return rootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ * Pauses the data refreshing when the fragment is not in focus.
+ */
+ @Override
+ public void onPause() {
+ refreshDataHandler.removeCallbacks(refreshTableTask);
+ super.onPause();
+ }
+
+ /**
+ * {@inheritDoc}
+ * Restarts the data refresh when the fragment returns to focus.
+ */
+ @Override
+ public void onResume() {
+ refreshDataHandler.postDelayed(refreshTableTask, REFRESH_TIME);
+ super.onResume();
+ }
+
+ /**
+ * {@inheritDoc}
+ * Obtains the constraint layout holding the sensor measurement values. Initialises the Recycler
+ * View for holding WiFi data and registers its Layout Manager.
+ */
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ sensorMeasurementList = (ConstraintLayout) getView().findViewById(R.id.sensorMeasurementList);
+ wifiListView = (RecyclerView) getView().findViewById(R.id.wifiList);
+ wifiListView.setLayoutManager(new LinearLayoutManager(getActivity()));
+
+ // 初始化视图
+ floorTextView = view.findViewById(R.id.Floor);
+
+ // 获取SensorFusion实例并注册为观察者
+ sensorFusion = SensorFusion.getInstance();
+ sensorFusion.registerFloorObserver(this);
+
+ // 检查布局中的卡片视图数量是否与传感器类型匹配
+ int cardViewCount = sensorMeasurementList.getChildCount();
+ int sensorTypeCount = SensorTypes.values().length;
+
+ if (cardViewCount < sensorTypeCount) {
+ Log.e("MeasurementsFragment", "布局中的CardView数量(" + cardViewCount +
+ ")小于SensorTypes枚举数量(" + sensorTypeCount + ")");
+ }
+
+ // 设置初始楼层值
+ updateFloorDisplay(sensorFusion.getCurrentFloor());
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ // 取消注册观察者
+ if (sensorFusion != null) {
+ sensorFusion.removeFloorObserver(this);
+ }
+
+ // 移除所有回调,防止内存泄漏
+ if (refreshDataHandler != null) {
+ refreshDataHandler.removeCallbacksAndMessages(null);
+ }
+
+ // 清空UI引用
+ sensorMeasurementList = null;
+ wifiListView = null;
+ floorTextView = null;
+ }
+
+ /**
+ * Runnable task containing functionality to update the UI with the relevant sensor data.
+ * Must be run on the UI thread via a Handler. Obtains movement sensor values and the current
+ * WiFi networks from the {@link SensorFusion} instance and updates the UI with the new data
+ * and the string wrappers provided.
+ *
+ * @see SensorFusion class handling all sensors and data processing.
+ * @see Wifi class holding network data.
+ */
+ private final Runnable refreshTableTask = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // 确保视图已经初始化
+ if (sensorMeasurementList == null || getActivity() == null) {
+ Log.e("MeasurementsFragment", "View not initialized or fragment detached");
+ return;
+ }
+
+ // Get all the values from SensorFusion
+ Map sensorValueMap = sensorFusion.getSensorValueMap();
+ // Loop through UI elements and update the values
+ for(SensorTypes st : SensorTypes.values()) {
+ // 检查索引是否有效
+ if (st.ordinal() >= sensorMeasurementList.getChildCount()) {
+ Log.e("MeasurementsFragment", "Invalid index: " + st.ordinal() +
+ ", ChildCount: " + sensorMeasurementList.getChildCount());
+ continue;
+ }
+
+ CardView cardView = (CardView) sensorMeasurementList.getChildAt(st.ordinal());
+ // 空值检查
+ if (cardView == null) {
+ Log.e("MeasurementsFragment", "CardView is null for sensor: " + st.name());
+ continue;
+ }
+
+ ConstraintLayout currentRow = (ConstraintLayout) cardView.getChildAt(0);
+ // 空值检查
+ if (currentRow == null) {
+ Log.e("MeasurementsFragment", "ConstraintLayout is null for sensor: " + st.name());
+ continue;
+ }
+
+ float[] values = sensorValueMap.get(st);
+ // 空值检查
+ if (values == null) {
+ Log.e("MeasurementsFragment", "Values array is null for sensor: " + st.name());
+ continue;
+ }
+
+ for (int i = 0; i < values.length; i++) {
+ // 检查索引有效性
+ if (i + 1 >= currentRow.getChildCount()) {
+ Log.e("MeasurementsFragment", "Invalid child index: " + (i + 1) +
+ " for sensor: " + st.name());
+ continue;
+ }
+
+ String valueString;
+ // Set string wrapper based on data type.
+ if(values.length == 1) {
+ valueString = getString(R.string.level, String.format("%.2f", values[0]));
+ }
+ else if(values.length == 2){
+ if(st == SensorTypes.GNSSLATLONG)
+ valueString = getString(gnssPrefaces[i], String.format("%.2f", values[i]));
+ else
+ valueString = getString(prefaces[i], String.format("%.2f", values[i]));
+ }
+ else{
+ valueString = getString(prefaces[i], String.format("%.2f", values[i]));
+ }
+
+ View childView = currentRow.getChildAt(i + 1);
+ if (childView instanceof TextView) {
+ ((TextView) childView).setText(valueString);
+ }
+ }
+ }
+
+ // Get all WiFi values - convert to list of strings
+ List wifiObjects = sensorFusion.getWifiList();
+ // If there are WiFi networks visible, update the recycler view with the data.
+ if(wifiObjects != null && wifiListView != null) {
+ wifiListView.setAdapter(new WifiListAdapter(getActivity(), wifiObjects));
+ }
+
+ // Restart the data updater task in REFRESH_TIME milliseconds.
+ refreshDataHandler.postDelayed(refreshTableTask, REFRESH_TIME);
+ } catch (Exception e) {
+ Log.e("MeasurementsFragment", "Error updating sensor data: " + e.getMessage());
+ // 即使发生错误,也确保继续刷新
+ if (refreshDataHandler != null) {
+ refreshDataHandler.postDelayed(refreshTableTask, REFRESH_TIME);
+ }
+ }
+ }
+ };
+
+ @Override
+ public void update(Object[] obj) {
+ if (obj.length > 0 && obj[0] instanceof Integer) {
+ int floor = (Integer) obj[0];
+ // 确保在主线程中更新UI
+ if (getActivity() != null) {
+ getActivity().runOnUiThread(() -> updateFloorDisplay(floor));
+ }
+ }
+ }
+
+ private void updateFloorDisplay(int floor) {
+ if (floorTextView != null) {
+ String oldText = floorTextView.getText().toString();
+ String displayText = sensorFusion.getFloorDisplay();
+ floorTextView.setText(displayText);
+ Log.d("FLOOR_UPDATE", String.format(
+ "Fragment UI更新 - 旧值: %s, 新值: %s (数值: %d)",
+ oldText,
+ displayText,
+ floor
+ ));
+ }
+ }
+
+ /**
+ * 设置基准气压值
+ * @param basePressure 新的基准气压值 (hPa)
+ */
+ public void setBasePressure(float basePressure) {
+ if (sensorFusion != null) {
+ sensorFusion.calibrateBasePressure(basePressure);
+ // 更新显示
+ updateFloorDisplay(sensorFusion.getCurrentFloor());
+ }
+ }
+
+ /**
+ * 在当前楼层校准气压计
+ * @param currentFloor 当前所在楼层
+ */
+ public void calibrateAtCurrentFloor(int currentFloor) {
+ if (sensorFusion != null) {
+ sensorFusion.calibrateAtKnownFloor(currentFloor);
+ // 更新显示
+ updateFloorDisplay(sensorFusion.getCurrentFloor());
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/NucleusBuildingManager.java b/app/src/main/java/com/openpositioning/PositionMe/fragments/NucleusBuildingManager.java
similarity index 77%
rename from app/src/main/java/com/openpositioning/PositionMe/utils/NucleusBuildingManager.java
rename to app/src/main/java/com/openpositioning/PositionMe/fragments/NucleusBuildingManager.java
index 3570e8ad..819bdd44 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/utils/NucleusBuildingManager.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/fragments/NucleusBuildingManager.java
@@ -1,26 +1,25 @@
-package com.openpositioning.PositionMe.utils;
+package com.openpositioning.PositionMe.fragments;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
import com.openpositioning.PositionMe.R;
-import com.openpositioning.PositionMe.presentation.fragment.IndoorMapFragment;
import java.util.ArrayList;
public class NucleusBuildingManager {
- private IndoorMapFragment indoorMapFragment;
+ private IndoorMapManager indoorMapManager;
private ArrayList buildingPolygon;
public NucleusBuildingManager(GoogleMap map) {
- // The nuclear building has 5 floors
- indoorMapFragment = new IndoorMapFragment(map, 5);
+// The nuclear building has 5 floors
+ indoorMapManager = new IndoorMapManager(map, 5);
- // southwest corner
+// southwest corner
double N1 = 55.92279;
double W1 = 3.174643;
- // Northeast corner
+// Northeast corner
double N2 = 55.92335;
double W2 = 3.173829;
@@ -32,15 +31,15 @@ public NucleusBuildingManager(GoogleMap map) {
buildingPolygon.add(new LatLng(N2, -W1)); // Northwest corner
// Initialize the indoor map of each layer
- indoorMapFragment.addFloor(0, R.drawable.floor_lg, new LatLngBounds(buildingPolygon.get(0), buildingPolygon.get(2)));
- indoorMapFragment.addFloor(1, R.drawable.floor_ug, new LatLngBounds(buildingPolygon.get(0), buildingPolygon.get(2)));
- indoorMapFragment.addFloor(2, R.drawable.floor_1, new LatLngBounds(buildingPolygon.get(0), buildingPolygon.get(2)));
- indoorMapFragment.addFloor(3, R.drawable.floor_2, new LatLngBounds(buildingPolygon.get(0), buildingPolygon.get(2)));
- indoorMapFragment.addFloor(4, R.drawable.floor_3, new LatLngBounds(buildingPolygon.get(0), buildingPolygon.get(2)));
+ indoorMapManager.addFloor(0, R.drawable.floor_lg, new LatLngBounds(buildingPolygon.get(0), buildingPolygon.get(2)));
+ indoorMapManager.addFloor(1, R.drawable.floor_ug, new LatLngBounds(buildingPolygon.get(0), buildingPolygon.get(2)));
+ indoorMapManager.addFloor(2, R.drawable.floor_1, new LatLngBounds(buildingPolygon.get(0), buildingPolygon.get(2)));
+ indoorMapManager.addFloor(3, R.drawable.floor_2, new LatLngBounds(buildingPolygon.get(0), buildingPolygon.get(2)));
+ indoorMapManager.addFloor(4, R.drawable.floor_3, new LatLngBounds(buildingPolygon.get(0), buildingPolygon.get(2)));
}
- public IndoorMapFragment getIndoorMapManager() {
- return indoorMapFragment;
+ public IndoorMapManager getIndoorMapManager() {
+ return indoorMapManager;
}
/**
diff --git a/app/src/main/java/com/openpositioning/PositionMe/fragments/RecordingFragment.java b/app/src/main/java/com/openpositioning/PositionMe/fragments/RecordingFragment.java
new file mode 100644
index 00000000..aa6eec5a
--- /dev/null
+++ b/app/src/main/java/com/openpositioning/PositionMe/fragments/RecordingFragment.java
@@ -0,0 +1,1076 @@
+package com.openpositioning.PositionMe.fragments;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Color;
+import android.location.Location;
+import android.os.Bundle;
+import android.os.CountDownTimer;
+import android.os.Handler;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.LinearInterpolator;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.Spinner;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.fragment.app.Fragment;
+import androidx.navigation.NavDirections;
+import androidx.navigation.Navigation;
+import androidx.preference.PreferenceManager;
+
+import com.google.android.gms.location.LocationCallback;
+import com.google.android.gms.location.LocationResult;
+import com.google.android.gms.maps.CameraUpdateFactory;
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.OnMapReadyCallback;
+import com.google.android.gms.maps.SupportMapFragment;
+import com.google.android.gms.maps.model.BitmapDescriptorFactory;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.Marker;
+import com.google.android.gms.maps.model.MarkerOptions;
+import com.google.android.gms.maps.model.Polyline;
+import com.google.android.gms.maps.model.PolylineOptions;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import com.openpositioning.PositionMe.IndoorMapManager;
+import com.openpositioning.PositionMe.R;
+import com.openpositioning.PositionMe.UtilFunctions;
+import com.openpositioning.PositionMe.sensors.SensorFusion;
+import com.openpositioning.PositionMe.sensors.SensorTypes;
+import com.openpositioning.PositionMe.utils.LocationLogger;
+import com.example.ekf.EKFManager;
+import com.example.ekf.GNSSProcessor;
+
+import java.util.List;
+
+/**
+ * A simple {@link Fragment} subclass. The recording fragment is displayed while the app is actively
+ * saving data, with UI elements and a map with a marker indicating current PDR location and
+ * direction of movement status. The user's PDR trajectory/path being recorded
+ * is drawn on the map as well.
+ * An overlay of indoor maps for the building is achieved when the user is in the Nucleus
+ * and Library buildings to allow for a better user experience.
+ *
+ * @see HomeFragment the previous fragment in the nav graph.
+ * @see CorrectionFragment the next fragment in the nav graph.
+ * @see SensorFusion the class containing sensors and recording.
+ * @see IndoorMapManager responsible for overlaying the indoor floor maps
+ *
+ * @author Mate Stodulka
+ * @author Arun Gopalakrishnan
+ */
+public class RecordingFragment extends Fragment implements OnMapReadyCallback {
+
+ //Button to end PDR recording
+ private Button stopButton;
+ private Button cancelButton;
+ //Recording icon to show user recording is in progress
+ private ImageView recIcon;
+ //Loading bar to show time remaining before recording automatically ends
+ private ProgressBar timeRemaining;
+ // Text views to display distance travelled and elevation since beginning of recording
+
+ private TextView elevation;
+ private TextView distanceTravelled;
+ // Text view to show the error between current PDR and current GNSS
+ private TextView gnssError;
+ // 初始化楼层显示文本框
+ private TextView floorTextView;
+
+ //App settings
+ private SharedPreferences settings;
+ //Singleton class to collect all sensor data
+ private SensorFusion sensorFusion;
+ //Timer to end recording
+ private CountDownTimer autoStop;
+ // Responsible for updating UI in Loop
+ private Handler refreshDataHandler;
+
+ //variables to store data of the trajectory
+ private float distance;
+ private float previousPosX;
+ private float previousPosY;
+
+ // Starting point coordinates
+ private static LatLng start;
+ // 起始位置数组
+ private float[] startPosition;
+ // Storing the google map object
+ private GoogleMap gMap;
+ //Switch Map Dropdown
+ private Spinner switchMapSpinner;
+ //Map Marker
+ private Marker orientationMarker;
+ // Current Location coordinates
+ private LatLng currentLocation;
+ // Next Location coordinates
+ private LatLng nextLocation;
+ // Stores the polyline object for plotting path
+ private Polyline polyline;
+ // Manages overlaying of the indoor maps
+ public IndoorMapManager indoorMapManager;
+ // Floor Up button
+ public FloatingActionButton floorUpButton;
+ // Floor Down button
+ public FloatingActionButton floorDownButton;
+ // GNSS Switch
+ private Switch gnss;
+ // EKF Switch
+ private Switch ekfSwitch;
+ // GNSS marker
+ private Marker gnssMarker;
+ // Button used to switch colour
+ private boolean isRed=true;
+ // Switch used to set auto floor
+ private Switch autoFloor;
+
+ private LocationLogger locationLogger;
+
+ private LocationCallback locationCallback;
+
+ // GNSS轨迹
+ private Polyline gnssPolyline;
+
+ // GNSS处理器
+ private GNSSProcessor gnssProcessor;
+
+ // GNSS历史位置 (用于避免重复添加相同位置)
+ private LatLng lastGnssPosition;
+
+ // 添加一个标志变量,跟踪是否已保存数据
+ private boolean locationDataSaved = false;
+
+ // EKF轨迹
+ private Polyline ekfPolyline;
+
+ /**
+ * Public Constructor for the class.
+ * Left empty as not required
+ */
+ public RecordingFragment() {
+ // Required empty public constructor
+ }
+
+ /**
+ * {@inheritDoc}
+ * Gets an instance of the {@link SensorFusion} class, and initialises the context and settings.
+ * Creates a handler for periodically updating the displayed data.
+ *
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ this.sensorFusion = SensorFusion.getInstance();
+ this.gnssProcessor = GNSSProcessor.getInstance();
+ Context context = getActivity();
+ this.settings = PreferenceManager.getDefaultSharedPreferences(context);
+ this.refreshDataHandler = new Handler();
+
+ // 初始化 LocationLogger
+ this.locationLogger = new LocationLogger(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ * Set title in action bar to "Recording"
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ // 调试开始
+ Log.d("RecordingFragment", "========= onCreateView开始 =========");
+
+ // Inflate the layout for this fragment
+ View rootView = inflater.inflate(R.layout.fragment_recording, container, false);
+ // Inflate the layout for this fragment
+ ((AppCompatActivity)getActivity()).getSupportActionBar().hide();
+ getActivity().setTitle("Recording...");
+
+ //Obtain start position set in the startLocation fragment
+ startPosition = sensorFusion.getGNSSLatitude(true);
+ Log.d("RecordingFragment", "获取到起始位置: " +
+ (startPosition != null ? startPosition[0] + ", " + startPosition[1] : "null"));
+
+ // 测试EKF和GPS
+ testPositioningSystem();
+
+ // Initialize map fragment
+ SupportMapFragment supportMapFragment = (SupportMapFragment)
+ getChildFragmentManager().findFragmentById(R.id.RecordingMap);
+ // Asynchronous map which can be configured
+ if (supportMapFragment != null) {
+ Log.d("RecordingFragment", "找到地图片段,异步加载地图");
+ supportMapFragment.getMapAsync(this);
+ } else {
+ Log.e("RecordingFragment", "无法找到地图片段");
+ }
+
+ Log.d("RecordingFragment", "========= onCreateView结束 =========");
+ return rootView;
+ }
+
+ /**
+ * 测试定位系统组件是否正常工作
+ */
+ private void testPositioningSystem() {
+ Log.d("PositioningTest", "========= 开始测试定位系统 =========");
+
+ // 1. 测试PDR数据
+ float[] pdrValues = sensorFusion.getSensorValueMap().get(SensorTypes.PDR);
+ if (pdrValues != null) {
+ Log.d("PositioningTest", String.format("PDR数据: X=%.6f, Y=%.6f", pdrValues[0], pdrValues[1]));
+ } else {
+ Log.e("PositioningTest", "PDR数据为null");
+ }
+
+ // 2. 测试GNSS数据
+ float[] gnssValues = sensorFusion.getSensorValueMap().get(SensorTypes.GNSSLATLONG);
+ if (gnssValues != null && gnssValues.length >= 2) {
+ Log.d("PositioningTest", String.format("GNSS数据: lat=%.8f, lng=%.8f",
+ gnssValues[0], gnssValues[1]));
+
+ // 测试GNSS处理器
+ LatLng gnssLocation = new LatLng(gnssValues[0], gnssValues[1]);
+ LatLng processedLocation = gnssProcessor.processGNSSPosition(gnssLocation);
+
+ Log.d("PositioningTest", String.format("处理后GNSS位置: lat=%.8f, lng=%.8f",
+ processedLocation.latitude, processedLocation.longitude));
+
+ // 将测试数据写入LocationLogger
+ locationLogger.logGnssLocation(
+ System.currentTimeMillis(),
+ processedLocation.latitude,
+ processedLocation.longitude
+ );
+ Log.d("PositioningTest", "已记录GNSS测试数据");
+ } else {
+ Log.e("PositioningTest", "GNSS数据无效: " +
+ (gnssValues == null ? "null" : "长度=" + gnssValues.length));
+ }
+
+ // 4. 测试PDR位置记录
+ Log.d("PositioningTest", "测试PDR位置记录...");
+ if (pdrValues != null) {
+ float[] pdrLongLat = sensorFusion.getPdrLongLat(pdrValues[0], pdrValues[1]);
+ Log.d("PositioningTest", String.format("记录PDR测试位置: lat=%.8f, lng=%.8f",
+ pdrLongLat[0], pdrLongLat[1]));
+
+ // 记录PDR位置
+ locationLogger.logLocation(
+ System.currentTimeMillis(),
+ pdrLongLat[0],
+ pdrLongLat[1]
+ );
+ }
+
+ Log.d("PositioningTest", "========= 测试定位系统结束 =========");
+ }
+
+ /**
+ * {@inheritDoc}
+ * Controls to allow scrolling, tilting, rotating and a compass view of the
+ * map are enabled. A marker is added to the map with the start position and
+ * the compass indicating user direction. A polyline object is initialised
+ * to plot user direction.
+ * Initialises the manager to control indoor floor map overlays.
+ *
+ * @param map Google map to be configured
+ */
+ @Override
+ public void onMapReady(GoogleMap map) {
+ Log.d("RecordingFragment", "地图准备就绪");
+ gMap = map;
+ //Initialising the indoor map manager object
+ indoorMapManager = new IndoorMapManager(map);
+ // Setting map attributes
+ map.setMapType(GoogleMap.MAP_TYPE_HYBRID);
+ map.getUiSettings().setCompassEnabled(true);
+ map.getUiSettings().setTiltGesturesEnabled(true);
+ map.getUiSettings().setRotateGesturesEnabled(true);
+ map.getUiSettings().setScrollGesturesEnabled(true);
+
+ // Add a marker at the start position and move the camera
+ start = new LatLng(startPosition[0], startPosition[1]);
+ currentLocation = start;
+ orientationMarker = map.addMarker(new MarkerOptions().position(start).title("Current Position")
+ .flat(true)
+ .icon(BitmapDescriptorFactory.fromBitmap(
+ UtilFunctions.getBitmapFromVector(getContext(),R.drawable.ic_baseline_navigation_24))));
+ //Center the camera
+ map.moveCamera(CameraUpdateFactory.newLatLngZoom(start, (float) 19f));
+ // Adding polyline to map to plot real-time trajectory
+ PolylineOptions polylineOptions = new PolylineOptions()
+ .color(Color.RED)
+ .add(currentLocation)
+ .zIndex(1000f);
+ polyline = gMap.addPolyline(polylineOptions);
+ // Setting current location to set Ground Overlay for indoor map (if in building)
+ indoorMapManager.setCurrentLocation(currentLocation);
+ //Showing an indication of available indoor maps using PolyLines
+ indoorMapManager.setIndicationOfIndoorMap();
+
+ Log.d("RecordingFragment", "地图初始化完成,当前位置: " + currentLocation.latitude + ", " + currentLocation.longitude);
+ }
+
+ /**
+ * {@inheritDoc}
+ * Text Views and Icons initialised to display the current PDR to the user. A Button onClick
+ * listener is enabled to detect when to go to next fragment and allow the user to correct PDR.
+ * Other onClick, onCheckedChange and onSelectedItem Listeners for buttons, switch and spinner
+ * are defined to allow user to change UI and functionality of the recording page as wanted
+ * by the user.
+ * A runnable thread is called to update the UI every 0.2 seconds.
+ */
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ // Set autoStop to null for repeat recordings
+ this.autoStop = null;
+
+ //Initialise UI components
+ this.elevation = getView().findViewById(R.id.currentElevation);
+ this.distanceTravelled = getView().findViewById(R.id.currentDistanceTraveled);
+ this.gnssError =getView().findViewById(R.id.gnssError);
+
+
+ //Set default text of TextViews to 0
+ this.gnssError.setVisibility(View.GONE);
+ this.elevation.setText(getString(R.string.elevation, "0"));
+ this.distanceTravelled.setText(getString(R.string.meter, "0"));
+
+ //Reset variables to 0
+ this.distance = 0f;
+ this.previousPosX = 0f;
+ this.previousPosY = 0f;
+
+ // Stop button to save trajectory and move to corrections
+ this.stopButton = getView().findViewById(R.id.stopButton);
+ this.stopButton.setOnClickListener(new View.OnClickListener() {
+ /**
+ * {@inheritDoc}
+ * OnClick listener for button to go to next fragment.
+ * When button clicked the PDR recording is stopped and the {@link CorrectionFragment} is loaded.
+ */
+ @Override
+ public void onClick(View view) {
+ if(autoStop != null) autoStop.cancel();
+ Log.d("RecordingFragment", "用户点击停止按钮,保存轨迹文件...");
+
+ // 首先更新并获取最新的融合位置
+ if (currentLocation != null) {
+ try {
+ // 记录GNSS位置
+ locationLogger.logGnssLocation(
+ System.currentTimeMillis(),
+ currentLocation.latitude,
+ currentLocation.longitude
+ );
+ Log.d("RecordingFragment", "保存前记录最后一个GNSS位置: " +
+ currentLocation.latitude + ", " + currentLocation.longitude);
+ } catch (Exception e) {
+ Log.e("RecordingFragment", "保存前更新位置数据时出错: " + e.getMessage(), e);
+ }
+ }
+
+ if (!locationDataSaved) {
+ try {
+ Log.d("RecordingFragment", "开始保存轨迹数据...");
+ locationLogger.saveToFile();
+ locationDataSaved = true;
+ Log.d("RecordingFragment", "轨迹文件保存成功");
+ } catch (Exception e) {
+ Log.e("RecordingFragment", "保存轨迹数据时出错: " + e.getMessage(), e);
+ }
+ } else {
+ Log.d("RecordingFragment", "轨迹数据已保存,跳过按钮点击中的保存");
+ }
+
+ stopRecording();
+ NavDirections action = RecordingFragmentDirections.actionRecordingFragmentToCorrectionFragment();
+ Navigation.findNavController(view).navigate(action);
+ }
+ });
+
+ // Cancel button to discard trajectory and return to Home
+ this.cancelButton = getView().findViewById(R.id.cancelButton);
+ this.cancelButton.setOnClickListener(new View.OnClickListener() {
+ /**
+ * {@inheritDoc}
+ * OnClick listener for button to go to home fragment.
+ * When button clicked the PDR recording is stopped and the {@link HomeFragment} is loaded.
+ * The trajectory is not saved.
+ */
+ @Override
+ public void onClick(View view) {
+ Log.d("RecordingFragment", "取消录制,不保存轨迹数据");
+ locationDataSaved = true; // 标记为已保存,防止onDestroy中保存数据
+ sensorFusion.stopRecording();
+ NavDirections action = RecordingFragmentDirections.actionRecordingFragmentToHomeFragment();
+ Navigation.findNavController(view).navigate(action);
+ if(autoStop != null) autoStop.cancel();
+ }
+ });
+ // Configuring dropdown for switching map types
+ mapDropdown();
+ // Setting listener for the switching map types dropdown
+ switchMap();
+ // Floor changer Buttons
+ this.floorUpButton=getView().findViewById(R.id.floorUpButton);
+ this.floorDownButton=getView().findViewById(R.id.floorDownButton);
+ // Auto-floor switch
+ this.autoFloor=getView().findViewById(R.id.autoFloor);
+ autoFloor.setChecked(true);
+ // Hiding floor changing buttons and auto-floor switch
+ setFloorButtonVisibility(View.GONE);
+ this.floorUpButton.setOnClickListener(new View.OnClickListener() {
+ /**
+ *{@inheritDoc}
+ * Listener for increasing the floor for the indoor map
+ */
+ @Override
+ public void onClick(View view) {
+ // Setting off auto-floor as manually changed
+ autoFloor.setChecked(false);
+ indoorMapManager.increaseFloor();
+ }
+ });
+ this.floorDownButton.setOnClickListener(new View.OnClickListener() {
+ /**
+ *{@inheritDoc}
+ * Listener for decreasing the floor for the indoor map
+ */
+ @Override
+ public void onClick(View view) {
+ // Setting off auto-floor as manually changed
+ autoFloor.setChecked(false);
+ indoorMapManager.decreaseFloor();
+ }
+ });
+ //Obtain the GNSS toggle switch
+ this.gnss = getView().findViewById(R.id.gnssSwitch);
+
+ //Obtain the EKF toggle switch
+ this.ekfSwitch = getView().findViewById(R.id.EKF_Switch);
+
+ this.gnss.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ /**
+ * {@inheritDoc}
+ * Listener to set GNSS marker and show GNSS vs PDR error.
+ */
+ @Override
+ public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
+ if (isChecked){
+ // Show GNSS eror
+ float[] location = sensorFusion.getSensorValueMap().get(SensorTypes.GNSSLATLONG);
+ LatLng gnssLocation = new LatLng(location[0],location[1]);
+ gnssError.setVisibility(View.VISIBLE);
+ gnssError.setText(String.format(getString(R.string.gnss_error)+"%.2fm",
+ UtilFunctions.distanceBetweenPoints(currentLocation,gnssLocation)));
+
+ // 使用GNSSProcessor处理GNSS位置
+ LatLng processedGnssLocation = gnssProcessor.processGNSSPosition(gnssLocation);
+ lastGnssPosition = processedGnssLocation; // 保存最后一个GNSS位置
+
+ // Set GNSS marker
+ gnssMarker=gMap.addMarker(
+ new MarkerOptions().title("GNSS position")
+ .position(processedGnssLocation)
+ .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE)));
+
+ // 创建GNSS轨迹线 (蓝色)
+ if (gnssPolyline == null) {
+ PolylineOptions gnssPolylineOptions = new PolylineOptions()
+ .color(Color.BLUE) // 蓝色表示GNSS轨迹
+ .add(processedGnssLocation)
+ .width(8f) // 宽度适中
+ .zIndex(1200f); // zIndex在PDR和EKF之间
+ gnssPolyline = gMap.addPolyline(gnssPolylineOptions);
+ }
+ } else {
+ gnssMarker.remove();
+ gnssMarker = null;
+ gnssError.setVisibility(View.GONE);
+
+ // 清除GNSS轨迹
+ if (gnssPolyline != null) {
+ gnssPolyline.remove();
+ gnssPolyline = null;
+ }
+
+ // 重置GNSS处理器
+ gnssProcessor.reset();
+ lastGnssPosition = null;
+ }
+ }
+ });
+
+ // EKF开关监听器
+ this.ekfSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (isChecked) {
+ // 启用EKF时初始化
+ if (currentLocation != null) {
+ // 创建EKF轨迹线
+ if (ekfPolyline == null) {
+ PolylineOptions ekfPolylineOptions = new PolylineOptions()
+ .color(Color.GREEN) // 使用绿色,表示EKF融合轨迹
+ .add(currentLocation)
+ .width(12f) // 增加宽度
+ .geodesic(true) // 平滑轨迹
+ .zIndex(1500f); // 确保EKF轨迹显示在PDR轨迹上方
+ ekfPolyline = gMap.addPolyline(ekfPolylineOptions);
+
+ // 仅在EKF初始启用时将地图中心设置为当前位置
+ gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(currentLocation, (float) 19f));
+ }
+
+ Log.d("RecordingFragment", "EKF enabled at: " + currentLocation.latitude + ", " + currentLocation.longitude);
+ } else {
+ Log.e("RecordingFragment", "Cannot enable EKF: current location is null");
+ }
+ } else {
+ // 禁用EKF时清除EKF轨迹
+ if (ekfPolyline != null) {
+ ekfPolyline.remove();
+ ekfPolyline = null;
+ }
+ }
+
+ Log.d("RecordingFragment", "EKF " + (isChecked ? "启用" : "禁用"));
+ }
+ });
+
+ // Display the progress of the recording when a max record length is set
+ this.timeRemaining = getView().findViewById(R.id.timeRemainingBar);
+
+ // Display a blinking red dot to show recording is in progress
+ blinkingRecording();
+
+ // Check if there is manually set time limit:
+ if(this.settings.getBoolean("split_trajectory", false)) {
+ // If that time limit has been reached:
+ long limit = this.settings.getInt("split_duration", 30) * 60000L;
+ // Set progress bar
+ this.timeRemaining.setMax((int) (limit/1000));
+ this.timeRemaining.setScaleY(3f);
+
+ // Create a CountDownTimer object to adhere to the time limit
+ this.autoStop = new CountDownTimer(limit, 1000) {
+ /**
+ * {@inheritDoc}
+ * Increment the progress bar to display progress and remaining time. Update the
+ * observed PDR values, and animate icons based on the data.
+ */
+ @Override
+ public void onTick(long l) {
+ // increment progress bar
+ timeRemaining.incrementProgressBy(1);
+ // Get new position and update UI
+ updateUIandPosition();
+ }
+
+ /**
+ * {@inheritDoc}
+ * Finish recording and move to the correction fragment.
+ * @see CorrectionFragment
+ */
+ @Override
+ public void onFinish() {
+ // Timer done, move to next fragment automatically - will stop recording
+ sensorFusion.stopRecording();
+ NavDirections action = RecordingFragmentDirections.actionRecordingFragmentToCorrectionFragment();
+ Navigation.findNavController(view).navigate(action);
+ }
+ }.start();
+ }
+ else {
+ // No time limit - use a repeating task to refresh UI.
+ this.refreshDataHandler.post(refreshDataTask);
+ }
+
+ // 在位置更新时记录
+ locationCallback = new LocationCallback() {
+ @Override
+ public void onLocationResult(LocationResult locationResult) {
+ if (locationResult == null) {
+ return;
+ }
+
+ for (Location location : locationResult.getLocations()) {
+ // 记录位置
+ locationLogger.logLocation(
+ location.getTime(),
+ location.getLatitude(),
+ location.getLongitude()
+ );
+
+ // 其他处理...
+ }
+ }
+ };
+
+ // 初始化楼层显示文本框
+ this.floorTextView = getView().findViewById(R.id.Floor);
+
+ // 添加状态改变监听器
+ autoFloor.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ Log.d("AutoFloor", "Switch state changed to: " + isChecked);
+
+ if (indoorMapManager == null) {
+ Log.d("AutoFloor", "IndoorMapManager is null");
+ return;
+ }
+
+ if (!indoorMapManager.getIsIndoorMapSet()) {
+ Log.d("AutoFloor", "No indoor map is currently set");
+ return;
+ }
+
+ if (isChecked) {
+ // 直接使用 SensorFusion 中的当前楼层
+ int currentFloor = sensorFusion.getCurrentFloor();
+ Log.d("AutoFloor", String.format(
+ "Switch ON - Using SensorFusion floor: %d",
+ currentFloor
+ ));
+ indoorMapManager.resumeAutoFloor(currentFloor);
+ } else {
+ Log.d("AutoFloor", "Switch turned OFF");
+ }
+ }
+ });
+ }
+
+ /**
+ * Creates a dropdown for Changing maps
+ */
+ private void mapDropdown(){
+ // Creating and Initialising options for Map's Dropdown Menu
+ switchMapSpinner = (Spinner) getView().findViewById(R.id.mapSwitchSpinner);
+ // Different Map Types
+ String[] maps = new String[]{getString(R.string.hybrid), getString(R.string.normal), getString(R.string.satellite)};
+ ArrayAdapter adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, maps);
+ // Set the Dropdowns menu adapter
+ switchMapSpinner.setAdapter(adapter);
+ }
+
+ /**
+ * Spinner listener to change map bap based on user input
+ */
+ private void switchMap(){
+ // Switch between map type based on user input
+ this.switchMapSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ /**
+ * {@inheritDoc}
+ * OnItemSelected listener to switch maps.
+ * The map switches between MAP_TYPE_NORMAL, MAP_TYPE_SATELLITE
+ * and MAP_TYPE_HYBRID based on user selection.
+ */
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ switch (position){
+ case 0:
+ gMap.setMapType(GoogleMap.MAP_TYPE_HYBRID);
+ break;
+ case 1:
+ gMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
+ break;
+ case 2:
+ gMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE);
+ break;
+ }
+ }
+ /**
+ * {@inheritDoc}
+ * When Nothing is selected set to MAP_TYPE_HYBRID (NORMAL and SATELLITE)
+ */
+ @Override
+ public void onNothingSelected(AdapterView> parent) {
+ gMap.setMapType(GoogleMap.MAP_TYPE_HYBRID);
+ }
+ });
+ }
+ /**
+ * Runnable task used to refresh UI elements with live data.
+ * Has to be run through a Handler object to be able to alter UI elements
+ */
+ private final Runnable refreshDataTask = new Runnable() {
+ @Override
+ public void run() {
+ // Get new position and update UI
+ updateUIandPosition();
+ // Loop the task again to keep refreshing the data
+ refreshDataHandler.postDelayed(refreshDataTask, 200);
+ }
+ };
+
+ /**
+ * Updates the UI, traces PDR Position on the map
+ * and also updates marker representing the current location and direction on the map
+ */
+ private void updateUIandPosition(){
+ // 调试:跟踪方法调用
+ long startTime = System.currentTimeMillis();
+ Log.d("LocationTracking", "======= 开始位置更新 =======");
+
+ // Get new position
+ float[] pdrValues = sensorFusion.getSensorValueMap().get(SensorTypes.PDR);
+ if (pdrValues == null) {
+ Log.e("LocationTracking", "PDR值为null,无法更新位置");
+ return;
+ }
+
+ // 调试:输出PDR原始值
+ Log.d("LocationTracking", String.format("PDR原始值: X=%.6f, Y=%.6f", pdrValues[0], pdrValues[1]));
+
+ // 计算PDR移动距离
+ float pdrMoveDist = (float) Math.sqrt(Math.pow(pdrValues[0] - previousPosX, 2) + Math.pow(pdrValues[1] - previousPosY, 2));
+
+ // 总距离计算
+ distance += pdrMoveDist;
+ distanceTravelled.setText(getString(R.string.meter, String.format("%.2f", distance)));
+
+ // Net pdr movement
+ float[] pdrMoved={pdrValues[0]-previousPosX,pdrValues[1]-previousPosY};
+
+ // 调试:输出PDR移动量
+ Log.d("LocationTracking", String.format("PDR移动量: dX=%.6f, dY=%.6f, 距离=%.6f",
+ pdrMoved[0], pdrMoved[1], pdrMoveDist));
+
+ // 设置一个最小移动阈值,防止微小抖动导致的误更新
+ final float MIN_MOVE_THRESHOLD = 0.15f; // 最小移动阈值(米)
+
+ // 只有PDR移动距离超过阈值才更新轨迹
+ if (pdrMoveDist > MIN_MOVE_THRESHOLD) {
+ // 调试:PDR位置已更新且超过移动阈值
+ Log.d("LocationTracking", "PDR移动距离("+pdrMoveDist+"m)超过阈值("+MIN_MOVE_THRESHOLD+"m),更新地图轨迹");
+
+ plotLines(pdrMoved);
+
+ // PDR数据更新,SensorFusion内部会自动进行融合计算
+ } else {
+ // 调试:PDR位置变化太小,不更新轨迹
+ Log.d("LocationTracking", "PDR移动距离("+pdrMoveDist+"m)小于阈值("+MIN_MOVE_THRESHOLD+"m),不更新轨迹");
+ }
+
+ // If not initialized, initialize
+ if (indoorMapManager == null) {
+ indoorMapManager = new IndoorMapManager(gMap);
+ }
+
+ // 处理GNSS位置更新 - 无论GNSS开关是否开启,都记录GNSS位置
+ float[] location = sensorFusion.getSensorValueMap().get(SensorTypes.GNSSLATLONG);
+ LatLng processedGnssLocation = null;
+
+ if (location != null && location.length >= 2 && location[0] != 0 && location[1] != 0) {
+ LatLng gnssLocation = new LatLng(location[0], location[1]);
+
+ // 调试:输出GNSS原始位置
+ Log.d("LocationTracking", String.format("GNSS原始位置: lat=%.8f, lng=%.8f",
+ gnssLocation.latitude, gnssLocation.longitude));
+
+ // 使用GNSSProcessor处理GNSS位置
+ processedGnssLocation = gnssProcessor.processGNSSPosition(gnssLocation);
+
+ // 调试:输出GNSS处理后位置
+ Log.d("LocationTracking", String.format("GNSS处理后位置: lat=%.8f, lng=%.8f",
+ processedGnssLocation.latitude, processedGnssLocation.longitude));
+
+ // 只有GNSS开关打开时才显示GNSS相关UI
+ if (gnss.isChecked() && gnssMarker != null) {
+ // 显示PDR与GNSS之间的误差
+ gnssError.setVisibility(View.VISIBLE);
+ gnssError.setText(String.format(getString(R.string.gnss_error)+"%.2fm",
+ UtilFunctions.distanceBetweenPoints(currentLocation, gnssLocation)));
+
+ // 更新GNSS标记位置
+ gnssMarker.setPosition(processedGnssLocation);
+
+ // 只有当位置有明显变化时才更新GNSS轨迹
+ if (lastGnssPosition == null ||
+ UtilFunctions.distanceBetweenPoints(lastGnssPosition, processedGnssLocation) > 0.5) {
+
+ // 更新GNSS轨迹UI
+ if (gnssPolyline != null) {
+ List gnssPoints = gnssPolyline.getPoints();
+ gnssPoints.add(processedGnssLocation);
+ gnssPolyline.setPoints(gnssPoints);
+ }
+
+ // 保存最新GNSS位置
+ lastGnssPosition = processedGnssLocation;
+ }
+ }
+
+ // 无论GNSS开关是否开启,都记录GNSS位置数据
+ // 每次更新都记录,不再使用距离过滤
+ locationLogger.logGnssLocation(
+ System.currentTimeMillis(),
+ processedGnssLocation.latitude,
+ processedGnssLocation.longitude
+ );
+ } else {
+ // 调试:GNSS位置无效
+ Log.e("LocationTracking", "GNSS位置无效或未获取: " +
+ (location == null ? "null" : "length=" + location.length));
+ }
+
+ // 获取EKF融合位置并记录 - 无论EKF开关是否开启
+ LatLng fusedPosition = sensorFusion.getEkfPosition();
+
+ // 调试:输出EKF融合位置
+ Log.d("LocationTracking", "EKF融合位置: " + (fusedPosition == null ? "null" :
+ fusedPosition.latitude + ", " + fusedPosition.longitude));
+
+ if (fusedPosition != null) {
+ // 记录融合位置
+ locationLogger.logEkfLocation(
+ System.currentTimeMillis(),
+ fusedPosition.latitude,
+ fusedPosition.longitude
+ );
+
+ // 只有当EKF开关打开时才更新EKF轨迹UI
+ if (ekfSwitch.isChecked() && ekfPolyline != null) {
+ List points = ekfPolyline.getPoints();
+ points.add(fusedPosition);
+ ekfPolyline.setPoints(points);
+ }
+ } else {
+ // EKF位置为null,尝试手动生成一个
+ if (currentLocation != null && processedGnssLocation != null) {
+ // 简单融合:50%PDR + 50%GNSS
+ double fusedLat = (currentLocation.latitude + processedGnssLocation.latitude) / 2;
+ double fusedLng = (currentLocation.longitude + processedGnssLocation.longitude) / 2;
+ LatLng manualFusedPosition = new LatLng(fusedLat, fusedLng);
+
+ Log.d("LocationTracking", "手动创建融合位置: " + fusedLat + ", " + fusedLng);
+
+ // 记录手动融合位置
+ locationLogger.logEkfLocation(
+ System.currentTimeMillis(),
+ manualFusedPosition.latitude,
+ manualFusedPosition.longitude
+ );
+ } else {
+ Log.e("LocationTracking", "无法创建手动融合位置,currentLocation或processedGnssLocation为null");
+ }
+ }
+
+ // Updates current location of user to show the indoor floor map (if applicable)
+ // 使用EKF融合位置来判断是否在室内
+ LatLng positionForIndoorMap = fusedPosition != null ? fusedPosition : currentLocation;
+ indoorMapManager.setCurrentLocation(positionForIndoorMap);
+
+ // 如果在室内且自动楼层开启,持续更新楼层
+ if (indoorMapManager.getIsIndoorMapSet()) {
+ setFloorButtonVisibility(View.VISIBLE);
+ if (autoFloor.isChecked()) {
+ // 直接使用 SensorFusion 中的当前楼层
+ int currentFloor = sensorFusion.getCurrentFloor();
+ Log.d("AutoFloor", "Auto updating - Current floor: " + currentFloor);
+ indoorMapManager.setCurrentFloor(currentFloor, true);
+ }
+ floorTextView.setText(sensorFusion.getFloorDisplay());
+ } else {
+ setFloorButtonVisibility(View.GONE);
+ floorTextView.setText(sensorFusion.getFloorDisplay());
+ }
+
+ // Store previous PDR values for next call
+ previousPosX = pdrValues[0];
+ previousPosY = pdrValues[1];
+ // Display elevation
+ elevation.setText(getString(R.string.elevation, String.format("%.1f", sensorFusion.getElevation())));
+ //Rotate compass Marker according to direction of movement
+ if (orientationMarker!=null) {
+ orientationMarker.setRotation((float) Math.toDegrees(sensorFusion.passOrientation()));
+ }
+
+ // 在位置更新时记录PDR位置到 LocationLogger
+ if (currentLocation != null) {
+ Log.d("LocationTracking", String.format("记录PDR位置: lat=%.8f, lng=%.8f",
+ currentLocation.latitude, currentLocation.longitude));
+
+ // 每次都记录当前PDR位置
+ locationLogger.logLocation(
+ System.currentTimeMillis(),
+ currentLocation.latitude,
+ currentLocation.longitude
+ );
+ } else {
+ Log.e("LocationTracking", "当前PDR位置为null,无法记录");
+ }
+
+ // 调试:跟踪方法结束
+ long endTime = System.currentTimeMillis();
+ Log.d("LocationTracking", String.format("======= 位置更新完成,耗时%dms =======", endTime - startTime));
+ }
+ /**
+ * Plots the users location based on movement in Real-time
+ * @param pdrMoved Contains the change in PDR in X and Y directions
+ */
+ private void plotLines(float[] pdrMoved){
+ if (currentLocation!=null){
+ // Calculate new position based on net PDR movement
+ nextLocation=UtilFunctions.calculateNewPos(currentLocation,pdrMoved);
+ //Try catch to prevent exceptions from crashing the app
+ try{
+ // Adds new location to polyline to plot the PDR path of user
+ List pointsMoved = polyline.getPoints();
+ pointsMoved.add(nextLocation);
+ polyline.setPoints(pointsMoved);
+ // 设置轨迹线的 zIndex 为较大值,确保显示在地图覆盖层上方
+ polyline.setZIndex(1000f);
+ // Change current location to new location and zoom there
+ orientationMarker.setPosition(nextLocation);
+ // 设置位置标记的 zIndex 也为较大值
+ orientationMarker.setZIndex(1000f);
+
+ // 获取最新的Fusion位置
+ LatLng fusedPosition = sensorFusion.getEkfPosition();
+ if (fusedPosition != null) {
+ // 移动相机到Fusion位置 (保持用户可以自由拖动地图)
+ gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(fusedPosition, (float) 19f));
+ }
+ }
+ catch (Exception ex){
+ Log.e("PlottingPDR","Exception: "+ex);
+ }
+ currentLocation=nextLocation;
+ }
+ else{
+ //Initialise the starting location
+ float[] location = sensorFusion.getGNSSLatitude(true);
+ currentLocation=new LatLng(location[0],location[1]);
+ nextLocation=currentLocation;
+ }
+ }
+
+ /**
+ * Function to set change visibility of the floor up and down buttons
+ * @param visibility the visibility of floor buttons should be set to
+ */
+ private void setFloorButtonVisibility(int visibility){
+ floorUpButton.setVisibility(visibility);
+ floorDownButton.setVisibility(visibility);
+ autoFloor.setVisibility(visibility);
+ }
+ /**
+ * Displays a blinking red dot to signify an ongoing recording.
+ *
+ * @see Animation for makin the red dot blink.
+ */
+ private void blinkingRecording() {
+ //Initialise Image View
+ this.recIcon = getView().findViewById(R.id.redDot);
+ //Configure blinking animation
+ Animation blinking_rec = new AlphaAnimation(1, 0);
+ blinking_rec.setDuration(800);
+ blinking_rec.setInterpolator(new LinearInterpolator());
+ blinking_rec.setRepeatCount(Animation.INFINITE);
+ blinking_rec.setRepeatMode(Animation.REVERSE);
+ recIcon.startAnimation(blinking_rec);
+ }
+
+ /**
+ * {@inheritDoc}
+ * Stops ongoing refresh task, but not the countdown timer which stops automatically
+ */
+ @Override
+ public void onPause() {
+ refreshDataHandler.removeCallbacks(refreshDataTask);
+ super.onPause();
+ }
+
+ /**
+ * {@inheritDoc}
+ * Restarts UI refreshing task when no countdown task is in progress
+ */
+ @Override
+ public void onResume() {
+ if(!this.settings.getBoolean("split_trajectory", false)) {
+ refreshDataHandler.postDelayed(refreshDataTask, 500);
+ }
+ super.onResume();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (!locationDataSaved) {
+ Log.d("RecordingFragment", "在onDestroy中保存轨迹数据");
+ locationLogger.saveToFile();
+ locationDataSaved = true;
+ } else {
+ Log.d("RecordingFragment", "轨迹数据已保存,跳过onDestroy中的保存");
+ }
+ }
+
+ private void stopRecording() {
+ Log.d("RecordingFragment", "停止录制");
+
+ // 保存轨迹数据
+ if (!locationDataSaved) {
+ Log.d("RecordingFragment", "在stopRecording中保存轨迹数据");
+ locationLogger.saveToFile();
+ locationDataSaved = true;
+ } else {
+ Log.d("RecordingFragment", "轨迹数据已保存,跳过stopRecording中的保存");
+ }
+
+ // 停止定时器
+ if (autoStop != null) {
+ autoStop.cancel();
+ autoStop = null;
+ }
+
+ // 停止刷新UI
+ refreshDataHandler.removeCallbacks(refreshDataTask);
+
+ // 清理资源
+ if (gnssMarker != null) {
+ gnssMarker.remove();
+ gnssMarker = null;
+ }
+
+ if (gnssPolyline != null) {
+ gnssPolyline.remove();
+ gnssPolyline = null;
+ }
+
+ if (ekfPolyline != null) {
+ ekfPolyline.remove();
+ ekfPolyline = null;
+ }
+
+ if (orientationMarker != null) {
+ orientationMarker.remove();
+ orientationMarker = null;
+ }
+
+ if (polyline != null) {
+ polyline.remove();
+ polyline = null;
+ }
+
+ Log.d("RecordingFragment", "录制已完全停止,资源已清理");
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/openpositioning/PositionMe/fragments/ReplayFragment.java b/app/src/main/java/com/openpositioning/PositionMe/fragments/ReplayFragment.java
new file mode 100644
index 00000000..33b53b25
--- /dev/null
+++ b/app/src/main/java/com/openpositioning/PositionMe/fragments/ReplayFragment.java
@@ -0,0 +1,711 @@
+package com.openpositioning.PositionMe.fragments;
+
+
+import android.graphics.Color;
+import android.location.Location;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+import android.widget.SeekBar;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.google.android.gms.maps.CameraUpdateFactory;
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.OnMapReadyCallback;
+import com.google.android.gms.maps.SupportMapFragment;
+import com.google.android.gms.maps.model.BitmapDescriptorFactory;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.Marker;
+import com.google.android.gms.maps.model.MarkerOptions;
+import com.google.android.gms.maps.model.Polyline;
+import com.google.android.gms.maps.model.PolylineOptions;
+import com.openpositioning.PositionMe.R;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class ReplayFragment extends Fragment implements OnMapReadyCallback {
+
+ private static final String TAG = "ReplayFragment";
+ private GoogleMap mMap;
+ private Button playPauseButton, restartButton;
+ private SeekBar progressBar;
+ private RadioGroup trajectoryTypeGroup;
+ private RadioButton pdrRadioButton, gnssRadioButton, ekfRadioButton;
+ private boolean isPlaying = false;
+
+ // 添加轨迹相关变量
+ private List trajectoryPoints = new ArrayList<>();
+ private List pdrTrajectoryPoints = new ArrayList<>();
+ private List gnssTrajectoryPoints = new ArrayList<>();
+ private List ekfTrajectoryPoints = new ArrayList<>();
+
+ private int currentPointIndex = 0;
+ private Handler playbackHandler = new Handler();
+ private static final int PLAYBACK_INTERVAL = 1000; // 1秒更新一次
+
+ // 轨迹平滑处理相关参数
+ private static final int DOWNSAMPLE_FACTOR = 3; // 每3个点取1个点
+ private static final boolean ENABLE_DOWNSAMPLING = true; // 启用降采样
+ private static final boolean ENABLE_SMOOTHING = true; // 启用平滑处理
+
+ private Marker currentPositionMarker;
+
+ private List pendingLocations = null; // 添加这个变量
+
+ // 当前显示的轨迹类型
+ private enum TrajectoryType {
+ PDR,
+ GNSS,
+ EKF
+ }
+
+ private TrajectoryType currentTrajectoryType = TrajectoryType.EKF; // 默认显示EKF轨迹
+
+ // 轨迹点类
+ private static class TrajectoryPoint {
+ long timestamp;
+ double latitude;
+ double longitude;
+
+ TrajectoryPoint(long timestamp, double latitude, double longitude) {
+ this.timestamp = timestamp;
+ this.latitude = latitude;
+ this.longitude = longitude;
+ }
+ }
+
+ public ReplayFragment() {
+ // Required empty public constructor
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ // Inflate the layout for this fragment
+ View view = inflater.inflate(R.layout.fragment_replay, container, false);
+
+ // Initialize UI elements
+ playPauseButton = view.findViewById(R.id.play_pause_button);
+ restartButton = view.findViewById(R.id.restart_button);
+ progressBar = (SeekBar) view.findViewById(R.id.progress_bar);
+
+ // 初始化轨迹类型选择按钮
+ trajectoryTypeGroup = view.findViewById(R.id.trajectory_type_group);
+ pdrRadioButton = view.findViewById(R.id.pdr_radio_button);
+ gnssRadioButton = view.findViewById(R.id.gnss_radio_button);
+ ekfRadioButton = view.findViewById(R.id.ekf_radio_button);
+
+ Log.d(TAG, "轨迹选择控件初始化: " +
+ "RadioGroup=" + (trajectoryTypeGroup != null) + ", " +
+ "PDR=" + (pdrRadioButton != null) + ", " +
+ "GNSS=" + (gnssRadioButton != null) + ", " +
+ "EKF=" + (ekfRadioButton != null));
+
+ if (trajectoryTypeGroup != null) {
+ // 设置点击事件监听器
+ trajectoryTypeGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(RadioGroup group, int checkedId) {
+ Log.d(TAG, "轨迹类型切换: checkedId=" + checkedId);
+
+ if (checkedId == R.id.pdr_radio_button) {
+ Log.d(TAG, "切换到PDR轨迹");
+ switchTrajectoryType(TrajectoryType.PDR);
+ } else if (checkedId == R.id.gnss_radio_button) {
+ Log.d(TAG, "切换到GNSS轨迹");
+ switchTrajectoryType(TrajectoryType.GNSS);
+ } else if (checkedId == R.id.ekf_radio_button) {
+ Log.d(TAG, "切换到EKF轨迹");
+ switchTrajectoryType(TrajectoryType.EKF);
+ }
+ }
+ });
+
+ // 单独为每个RadioButton设置点击监听器,以防RadioGroup监听器失效
+ pdrRadioButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Log.d(TAG, "PDR RadioButton 点击");
+ pdrRadioButton.setChecked(true);
+ switchTrajectoryType(TrajectoryType.PDR);
+ }
+ });
+
+ gnssRadioButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Log.d(TAG, "GNSS RadioButton 点击");
+ gnssRadioButton.setChecked(true);
+ switchTrajectoryType(TrajectoryType.GNSS);
+ }
+ });
+
+ ekfRadioButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Log.d(TAG, "EKF RadioButton 点击");
+ ekfRadioButton.setChecked(true);
+ switchTrajectoryType(TrajectoryType.EKF);
+ }
+ });
+ } else {
+ Log.e(TAG, "轨迹类型RadioGroup未找到!");
+ }
+
+ // Set button click listeners
+ playPauseButton.setOnClickListener(v -> togglePlayback());
+ restartButton.setOnClickListener(v -> restartPlayback());
+
+ // Set up the map
+ SupportMapFragment mapFragment = (SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.map);
+ if (mapFragment != null) {
+ mapFragment.getMapAsync(this);
+ }
+
+ // 设置进度条监听器
+ progressBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (fromUser) {
+ currentPointIndex = progress;
+ if (!trajectoryPoints.isEmpty() && progress < trajectoryPoints.size()) {
+ TrajectoryPoint point = trajectoryPoints.get(progress);
+ LatLng position = new LatLng(point.latitude, point.longitude);
+
+ // 更新标记位置
+ if (currentPositionMarker == null) {
+ currentPositionMarker = mMap.addMarker(new MarkerOptions()
+ .position(position)
+ .title("Current Position")
+ .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE)));
+ } else {
+ currentPositionMarker.setPosition(position);
+ }
+
+ // 移动相机
+ mMap.animateCamera(CameraUpdateFactory.newLatLng(position));
+ }
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ // 开始拖动时暂停播放
+ if (isPlaying) {
+ togglePlayback();
+ }
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ // 可以选择在停止拖动时自动开始播放
+ // if (!isPlaying) {
+ // togglePlayback();
+ // }
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ // 获取传入的文件路径
+ String filePath = ReplayFragmentArgs.fromBundle(getArguments()).getFilePath();
+ Log.d(TAG, "Received file path: " + filePath);
+
+ // 如果没有指定文件路径,使用最新的本地轨迹文件
+ if (filePath == null || filePath.isEmpty()) {
+ File directory = new File(requireContext().getExternalFilesDir(null), "location_logs");
+ File[] files = directory.listFiles((dir, name) -> name.startsWith("location_log_local_"));
+ if (files != null && files.length > 0) {
+ // 按修改时间排序,获取最新的文件
+ File latestFile = files[0];
+ for (File file : files) {
+ if (file.lastModified() > latestFile.lastModified()) {
+ latestFile = file;
+ }
+ }
+ filePath = latestFile.getAbsolutePath();
+ Log.d(TAG, "Using latest local trajectory file: " + filePath);
+ }
+ }
+
+ try {
+ // 读取轨迹文件
+ File trajectoryFile = new File(filePath);
+ StringBuilder content = new StringBuilder();
+ try (BufferedReader reader = new BufferedReader(new FileReader(trajectoryFile))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ content.append(line);
+ }
+ }
+
+ // 解析 JSON
+ JSONObject jsonObject = new JSONObject(content.toString());
+
+ // 优先读取EKF轨迹数据
+ boolean hasValidTrajectory = false;
+
+ // 读取EKF轨迹数据
+ if (jsonObject.has("ekfLocationData")) {
+ JSONArray ekfLocationData = jsonObject.getJSONArray("ekfLocationData");
+ if (ekfLocationData.length() > 0) {
+ hasValidTrajectory = true;
+ parseTrajectoryData(ekfLocationData, ekfTrajectoryPoints);
+ Log.d(TAG, "Successfully loaded EKF trajectory: " + ekfTrajectoryPoints.size() + " points");
+ ekfRadioButton.setEnabled(true);
+ } else {
+ ekfRadioButton.setEnabled(false);
+ }
+ } else {
+ ekfRadioButton.setEnabled(false);
+ }
+
+ // 读取GNSS轨迹数据
+ if (jsonObject.has("gnssLocationData")) {
+ JSONArray gnssLocationData = jsonObject.getJSONArray("gnssLocationData");
+ if (gnssLocationData.length() > 0) {
+ hasValidTrajectory = true;
+ parseTrajectoryData(gnssLocationData, gnssTrajectoryPoints);
+ Log.d(TAG, "Successfully loaded GNSS trajectory: " + gnssTrajectoryPoints.size() + " points");
+ gnssRadioButton.setEnabled(true);
+ } else {
+ gnssRadioButton.setEnabled(false);
+ }
+ } else {
+ gnssRadioButton.setEnabled(false);
+ }
+
+ // 读取PDR轨迹数据
+ if (jsonObject.has("locationData")) {
+ JSONArray locationData = jsonObject.getJSONArray("locationData");
+ if (locationData.length() > 0) {
+ hasValidTrajectory = true;
+ parseTrajectoryData(locationData, pdrTrajectoryPoints);
+ Log.d(TAG, "Successfully loaded PDR trajectory: " + pdrTrajectoryPoints.size() + " points");
+ pdrRadioButton.setEnabled(true);
+ } else {
+ pdrRadioButton.setEnabled(false);
+ }
+ } else {
+ pdrRadioButton.setEnabled(false);
+ }
+
+ if (!hasValidTrajectory) {
+ Toast.makeText(getContext(), "No valid trajectory data found", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 设置默认轨迹类型
+ Log.d(TAG, "设置默认轨迹类型 - EKF轨迹可用: " + ekfRadioButton.isEnabled() +
+ ", GNSS轨迹可用: " + gnssRadioButton.isEnabled() +
+ ", PDR轨迹可用: " + pdrRadioButton.isEnabled());
+
+ if (ekfRadioButton.isEnabled()) {
+ currentTrajectoryType = TrajectoryType.EKF;
+ ekfRadioButton.setChecked(true);
+ gnssRadioButton.setChecked(false);
+ pdrRadioButton.setChecked(false);
+ trajectoryPoints = ekfTrajectoryPoints;
+ Log.d(TAG, "默认显示EKF轨迹,轨迹点数量: " + trajectoryPoints.size());
+ } else if (gnssRadioButton.isEnabled()) {
+ currentTrajectoryType = TrajectoryType.GNSS;
+ gnssRadioButton.setChecked(true);
+ ekfRadioButton.setChecked(false);
+ pdrRadioButton.setChecked(false);
+ trajectoryPoints = gnssTrajectoryPoints;
+ Log.d(TAG, "默认显示GNSS轨迹,轨迹点数量: " + trajectoryPoints.size());
+ } else if (pdrRadioButton.isEnabled()) {
+ currentTrajectoryType = TrajectoryType.PDR;
+ pdrRadioButton.setChecked(true);
+ ekfRadioButton.setChecked(false);
+ gnssRadioButton.setChecked(false);
+ trajectoryPoints = pdrTrajectoryPoints;
+ Log.d(TAG, "默认显示PDR轨迹,轨迹点数量: " + trajectoryPoints.size());
+ }
+
+ // 如果地图已经准备好,直接更新
+ if (mMap != null) {
+ updateMap();
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to load trajectory file: " + e.getMessage());
+ Toast.makeText(getContext(), "Failed to load trajectory file: " + e.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * 解析轨迹数据并添加到指定列表
+ */
+ private void parseTrajectoryData(JSONArray jsonArray, List targetList) throws Exception {
+ targetList.clear();
+
+ if (jsonArray.length() == 0) {
+ return;
+ }
+
+ // 原始点集合
+ List originalPoints = new ArrayList<>();
+
+ // 先解析所有原始点
+ for (int i = 0; i < jsonArray.length(); i++) {
+ JSONObject point = jsonArray.getJSONObject(i);
+ originalPoints.add(new TrajectoryPoint(
+ point.getLong("timestamp"),
+ point.getDouble("latitude"),
+ point.getDouble("longitude")
+ ));
+ }
+
+ // 按时间戳排序
+ Collections.sort(originalPoints, (p1, p2) -> Long.compare(p1.timestamp, p2.timestamp));
+
+ // 对于PDR轨迹,保留所有原始点
+ if (targetList == pdrTrajectoryPoints) {
+ targetList.addAll(originalPoints);
+ Log.d(TAG, "PDR轨迹加载完成,保留所有原始点: " + targetList.size() + " 个点");
+ return;
+ }
+
+ // 对于其他轨迹类型,进行降采样处理
+ if (ENABLE_DOWNSAMPLING && originalPoints.size() > 100) {
+ Log.d(TAG, "原始点数: " + originalPoints.size() + ",进行降采样处理");
+
+ // 每DOWNSAMPLE_FACTOR个点取一个点
+ for (int i = 0; i < originalPoints.size(); i += DOWNSAMPLE_FACTOR) {
+ targetList.add(originalPoints.get(i));
+ }
+
+ // 确保包含最后一个点
+ if (targetList.isEmpty() || targetList.get(targetList.size()-1) != originalPoints.get(originalPoints.size()-1)) {
+ targetList.add(originalPoints.get(originalPoints.size()-1));
+ }
+
+ Log.d(TAG, "降采样后点数: " + targetList.size());
+ } else {
+ // 点数较少,不降采样,直接使用原始点
+ targetList.addAll(originalPoints);
+ }
+
+ // 对GNSS轨迹进行额外平滑处理
+ if (ENABLE_SMOOTHING && targetList == gnssTrajectoryPoints && targetList.size() > 5) {
+ Log.d(TAG, "对GNSS轨迹执行平滑处理");
+ smoothGnssTrajectory(targetList);
+ }
+ }
+
+ /**
+ * 对GNSS轨迹进行平滑处理
+ */
+ private void smoothGnssTrajectory(List points) {
+ if (points.size() < 5) return;
+
+ // 复制原始点集合
+ List originalPoints = new ArrayList<>(points);
+ points.clear();
+
+ // 移动平均窗口宽度
+ int windowSize = 5;
+ double[] weights = {0.1, 0.2, 0.4, 0.2, 0.1}; // 加权移动平均权重
+
+ // 处理开头的点(直接保留)
+ for (int i = 0; i < windowSize/2; i++) {
+ points.add(originalPoints.get(i));
+ }
+
+ // 对中间的点应用加权移动平均
+ for (int i = windowSize/2; i < originalPoints.size() - windowSize/2; i++) {
+ double sumLat = 0, sumLng = 0;
+ for (int j = 0; j < windowSize; j++) {
+ int idx = i - windowSize/2 + j;
+ TrajectoryPoint pt = originalPoints.get(idx);
+ sumLat += pt.latitude * weights[j];
+ sumLng += pt.longitude * weights[j];
+ }
+
+ TrajectoryPoint smoothedPoint = new TrajectoryPoint(
+ originalPoints.get(i).timestamp,
+ sumLat,
+ sumLng
+ );
+ points.add(smoothedPoint);
+ }
+
+ // 处理结尾的点(直接保留)
+ for (int i = originalPoints.size() - windowSize/2; i < originalPoints.size(); i++) {
+ points.add(originalPoints.get(i));
+ }
+
+ Log.d(TAG, "GNSS轨迹平滑处理完成");
+ }
+
+ /**
+ * 切换轨迹类型
+ */
+ private void switchTrajectoryType(TrajectoryType type) {
+ if (mMap == null) {
+ Log.e(TAG, "地图尚未准备好,无法切换轨迹类型");
+ return;
+ }
+
+ Log.d(TAG, "切换轨迹类型: " + type.name());
+
+ // 停止播放并重置位置
+ stopPlayback();
+ currentPointIndex = 0;
+
+ currentTrajectoryType = type;
+
+ // 更新轨迹点和RadioButton状态
+ switch (type) {
+ case PDR:
+ trajectoryPoints = pdrTrajectoryPoints;
+ Log.d(TAG, "切换到PDR轨迹,轨迹点数量: " + trajectoryPoints.size());
+ if (!pdrRadioButton.isChecked()) {
+ pdrRadioButton.setChecked(true);
+ gnssRadioButton.setChecked(false);
+ ekfRadioButton.setChecked(false);
+ }
+ break;
+ case GNSS:
+ trajectoryPoints = gnssTrajectoryPoints;
+ Log.d(TAG, "切换到GNSS轨迹,轨迹点数量: " + trajectoryPoints.size());
+ if (!gnssRadioButton.isChecked()) {
+ gnssRadioButton.setChecked(true);
+ pdrRadioButton.setChecked(false);
+ ekfRadioButton.setChecked(false);
+ }
+ break;
+ case EKF:
+ trajectoryPoints = ekfTrajectoryPoints;
+ Log.d(TAG, "切换到EKF轨迹,轨迹点数量: " + trajectoryPoints.size());
+ if (!ekfRadioButton.isChecked()) {
+ ekfRadioButton.setChecked(true);
+ pdrRadioButton.setChecked(false);
+ gnssRadioButton.setChecked(false);
+ }
+ break;
+ }
+
+ // 更新地图和进度条
+ updateMap();
+
+ // 确保进度条正确反映轨迹长度
+ if (progressBar != null) {
+ progressBar.setMax(trajectoryPoints.size() > 0 ? trajectoryPoints.size() - 1 : 0);
+ progressBar.setProgress(0);
+ }
+
+ Log.d(TAG, "轨迹类型切换完成: " + type.name());
+ }
+
+ @Override
+ public void onMapReady(GoogleMap googleMap) {
+ mMap = googleMap;
+
+ // 设置地图类型为卫星地图
+ mMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE);
+
+ // 如果已经有轨迹数据,更新地图显示
+ if (!trajectoryPoints.isEmpty()) {
+ updateMap();
+ }
+ }
+
+ private void togglePlayback() {
+ isPlaying = !isPlaying;
+ if (isPlaying) {
+ playPauseButton.setText(R.string.pause);
+ startPlayback();
+ } else {
+ playPauseButton.setText(R.string.play);
+ stopPlayback();
+ }
+ }
+
+ private void startPlayback() {
+ playbackHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (isPlaying && currentPointIndex < trajectoryPoints.size()) {
+ // 更新地图上的位置
+ TrajectoryPoint point = trajectoryPoints.get(currentPointIndex);
+ LatLng position = new LatLng(point.latitude, point.longitude);
+
+ // 更新或创建当前位置标记
+ if (currentPositionMarker == null) {
+ currentPositionMarker = mMap.addMarker(new MarkerOptions()
+ .position(position)
+ .title("Current Position")
+ .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE)));
+ } else {
+ currentPositionMarker.setPosition(position);
+ }
+
+ // 移动相机
+ mMap.animateCamera(CameraUpdateFactory.newLatLng(position));
+
+ // 更新进度条
+ progressBar.setProgress(currentPointIndex);
+
+ currentPointIndex++;
+ playbackHandler.postDelayed(this, PLAYBACK_INTERVAL);
+ } else {
+ isPlaying = false;
+ playPauseButton.setText(R.string.play);
+ }
+ }
+ }, PLAYBACK_INTERVAL);
+ }
+
+ private void stopPlayback() {
+ playbackHandler.removeCallbacksAndMessages(null);
+ }
+
+ private void restartPlayback() {
+ stopPlayback();
+ currentPointIndex = 0;
+ progressBar.setProgress(0);
+ playPauseButton.setText(R.string.play);
+ isPlaying = false;
+
+ // 移动相机回到起点
+ if (!trajectoryPoints.isEmpty()) {
+ TrajectoryPoint startPoint = trajectoryPoints.get(0);
+ LatLng startPosition = new LatLng(startPoint.latitude, startPoint.longitude);
+
+ // 更新位置标记
+ if (currentPositionMarker != null) {
+ currentPositionMarker.setPosition(startPosition);
+ } else {
+ currentPositionMarker = mMap.addMarker(new MarkerOptions()
+ .position(startPosition)
+ .title("Current Position")
+ .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE)));
+ }
+
+ // 移动相机
+ mMap.animateCamera(CameraUpdateFactory.newLatLng(startPosition));
+ }
+ }
+
+ private void updateMap() {
+ if (mMap == null || trajectoryPoints == null || trajectoryPoints.isEmpty()) {
+ Log.e(TAG, "无法更新地图:地图未准备好或轨迹点为空");
+ return;
+ }
+
+ // 清除现有轨迹和标记
+ if (currentPositionMarker != null) {
+ currentPositionMarker.remove();
+ currentPositionMarker = null;
+ }
+
+ // 清除地图上所有现有的轨迹线
+ mMap.clear();
+
+ // 根据轨迹类型设置颜色
+ int trajectoryColor;
+ switch (currentTrajectoryType) {
+ case PDR:
+ trajectoryColor = Color.RED;
+ break;
+ case GNSS:
+ trajectoryColor = Color.BLUE;
+ break;
+ case EKF:
+ trajectoryColor = Color.GREEN;
+ break;
+ default:
+ trajectoryColor = Color.RED;
+ }
+
+ // 创建新的轨迹线
+ PolylineOptions polylineOptions = new PolylineOptions()
+ .color(trajectoryColor)
+ .width(8f)
+ .geodesic(true);
+
+ // 添加轨迹点
+ for (TrajectoryPoint point : trajectoryPoints) {
+ polylineOptions.add(new LatLng(point.latitude, point.longitude));
+ }
+
+ // 添加轨迹线到地图
+ Polyline polyline = mMap.addPolyline(polylineOptions);
+
+ // 设置当前点标记
+ if (!trajectoryPoints.isEmpty()) {
+ TrajectoryPoint currentPoint = trajectoryPoints.get(currentPointIndex);
+ currentPositionMarker = mMap.addMarker(new MarkerOptions()
+ .position(new LatLng(currentPoint.latitude, currentPoint.longitude))
+ .title("当前位置")
+ .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE)));
+ }
+
+ // 更新进度条
+ if (progressBar != null) {
+ progressBar.setMax(trajectoryPoints.size() - 1);
+ progressBar.setProgress(currentPointIndex);
+ }
+
+ // 计算轨迹边界
+ if (!trajectoryPoints.isEmpty()) {
+ double minLat = Double.MAX_VALUE;
+ double maxLat = Double.MIN_VALUE;
+ double minLng = Double.MAX_VALUE;
+ double maxLng = Double.MIN_VALUE;
+
+ for (TrajectoryPoint point : trajectoryPoints) {
+ minLat = Math.min(minLat, point.latitude);
+ maxLat = Math.max(maxLat, point.latitude);
+ minLng = Math.min(minLng, point.longitude);
+ maxLng = Math.max(maxLng, point.longitude);
+ }
+
+ // 计算合适的缩放级别
+ float zoomLevel = 19f; // 默认缩放级别
+ double latDiff = maxLat - minLat;
+ double lngDiff = maxLng - minLng;
+
+ // 根据轨迹范围调整缩放级别
+ if (latDiff > 0.01 || lngDiff > 0.01) {
+ zoomLevel = 18f; // 增大缩放级别
+ } else if (latDiff > 0.005 || lngDiff > 0.005) {
+ zoomLevel = 19f; // 增大缩放级别
+ } else {
+ zoomLevel = 20f; // 对于更小的范围使用更大的缩放级别
+ }
+
+ // 使用轨迹的第一个点作为中心点
+ TrajectoryPoint firstPoint = trajectoryPoints.get(0);
+ LatLng center = new LatLng(firstPoint.latitude, firstPoint.longitude);
+
+ // 使用animateCamera进行平滑过渡
+ mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(center, zoomLevel), 1000, null);
+ }
+ }
+}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/SettingsFragment.java b/app/src/main/java/com/openpositioning/PositionMe/fragments/SettingsFragment.java
similarity index 97%
rename from app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/SettingsFragment.java
rename to app/src/main/java/com/openpositioning/PositionMe/fragments/SettingsFragment.java
index c1f6501c..c562d976 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/SettingsFragment.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/fragments/SettingsFragment.java
@@ -1,4 +1,4 @@
-package com.openpositioning.PositionMe.presentation.fragment;
+package com.openpositioning.PositionMe.fragments;
import android.os.Bundle;
import android.text.InputType;
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/StartLocationFragment.java b/app/src/main/java/com/openpositioning/PositionMe/fragments/StartLocationFragment.java
similarity index 60%
rename from app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/StartLocationFragment.java
rename to app/src/main/java/com/openpositioning/PositionMe/fragments/StartLocationFragment.java
index ee14f69f..c2a0e12e 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/StartLocationFragment.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/fragments/StartLocationFragment.java
@@ -1,4 +1,4 @@
-package com.openpositioning.PositionMe.presentation.fragment;
+package com.openpositioning.PositionMe.fragments;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -10,6 +10,8 @@
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
+import androidx.navigation.NavDirections;
+import androidx.navigation.Navigation;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
@@ -19,36 +21,32 @@
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.openpositioning.PositionMe.R;
-import com.openpositioning.PositionMe.presentation.activity.RecordingActivity;
-import com.openpositioning.PositionMe.presentation.activity.ReplayActivity;
import com.openpositioning.PositionMe.sensors.SensorFusion;
-import com.openpositioning.PositionMe.utils.NucleusBuildingManager;
/**
* A simple {@link Fragment} subclass. The startLocation fragment is displayed before the trajectory
* recording starts. This fragment displays a map in which the user can adjust their location to
* correct the PDR when it is complete
*
- * @author Virginia Cangelosi
* @see HomeFragment the previous fragment in the nav graph.
* @see RecordingFragment the next fragment in the nav graph.
* @see SensorFusion the class containing sensors and recording.
+ *
+ * @author Virginia Cangelosi
*/
public class StartLocationFragment extends Fragment {
- // Button to go to next fragment and save the location
+ //Button to go to next fragment and save the location
private Button button;
- // Singleton SensorFusion class which stores data from all sensors
+ //Singleton SesnorFusion class which stores data from all sensors
private SensorFusion sensorFusion = SensorFusion.getInstance();
- // Google maps LatLng object to pass location to the map
+ //Google maps LatLong object to pass location to the map
private LatLng position;
- // Start position of the user to be stored
+ //Start position of the user to be stored
private float[] startPosition = new float[2];
- // Zoom level for the Google map
+ //Zoom of google maps
+ private NucleusBuildingManager NucleusBuildingManager;
private float zoom = 19f;
- // Instance for managing indoor building overlays (if any)
- private NucleusBuildingManager nucleusBuildingManager;
- // Dummy variable for floor index
private int FloorNK;
/**
@@ -66,25 +64,42 @@ public StartLocationFragment() {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
- AppCompatActivity activity = (AppCompatActivity) getActivity();
- if (activity != null && activity.getSupportActionBar() != null) {
- activity.getSupportActionBar().hide();
- }
+ // Inflate the layout for this fragment
+ ((AppCompatActivity)getActivity()).getSupportActionBar().hide();
View rootView = inflater.inflate(R.layout.fragment_startlocation, container, false);
- // Obtain the start position from the GPS data from the SensorFusion class
+ //Obtain the start position from the GPS data from the SensorFusion class
startPosition = sensorFusion.getGNSSLatitude(false);
- // If no location found, zoom the map out
- if (startPosition[0] == 0 && startPosition[1] == 0) {
+ //If not location found zoom the map out
+ if(startPosition[0]==0 && startPosition[1]==0){
zoom = 1f;
- } else {
+ }
+ else {
zoom = 19f;
}
-
// Initialize map fragment
- SupportMapFragment supportMapFragment = (SupportMapFragment)
+ SupportMapFragment supportMapFragment=(SupportMapFragment)
getChildFragmentManager().findFragmentById(R.id.startMap);
+
+ // This is just a demonstration of the automatic expansion of the indoor map.
+ // Assume that we have obtained the user's position "newPosition" from the callback function. >>>
+
+// if (newPosition != null) {
+// // Check if the user's position is inside the defined building polygon
+// if (NucleusBuildingManager.isPointInBuilding(newPosition)) {
+// FloorButtons.setVisibility(View.VISIBLE);
+// switchFloorNU(floor);
+// InNu = 1; // Mark indoor map status
+// } else {
+// NucleusBuildingManager.getIndoorMapManager().hideMap();
+// FloorButtons.setVisibility(View.GONE);
+// InNu = 0; // Mark indoor map status
+// }
+// }
+
+
+ // Asynchronous map which can be configured
supportMapFragment.getMapAsync(new OnMapReadyCallback() {
/**
* {@inheritDoc}
@@ -95,42 +110,40 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container,
*/
@Override
public void onMapReady(GoogleMap mMap) {
- // Set map type and UI settings
mMap.setMapType(GoogleMap.MAP_TYPE_HYBRID);
mMap.getUiSettings().setCompassEnabled(true);
mMap.getUiSettings().setTiltGesturesEnabled(true);
mMap.getUiSettings().setRotateGesturesEnabled(true);
mMap.getUiSettings().setScrollGesturesEnabled(true);
- // *** FIX: Clear any existing markers so the start marker isn’t duplicated ***
- mMap.clear();
- // Create NucleusBuildingManager instance (if needed)
- nucleusBuildingManager = new NucleusBuildingManager(mMap);
- nucleusBuildingManager.getIndoorMapManager().hideMap();
+ if (mMap != null) {
+ // Create NuclearBuildingManager instance
+ NucleusBuildingManager = new NucleusBuildingManager(mMap);
+ NucleusBuildingManager.getIndoorMapManager().hideMap();
+ }
- // Add a marker at the current GPS location and move the camera
+ // Add a marker in current GPS location and move the camera
position = new LatLng(startPosition[0], startPosition[1]);
- Marker startMarker = mMap.addMarker(new MarkerOptions()
- .position(position)
- .title("Start Position")
- .draggable(true));
- mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(position, zoom));
-
- // Drag listener for the marker to update the start position when dragged
- mMap.setOnMarkerDragListener(new GoogleMap.OnMarkerDragListener() {
+ mMap.addMarker(new MarkerOptions().position(position).title("Start Position")).setDraggable(true);
+ mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(position, zoom ));
+
+ //Drag listener for the marker to execute when the markers location is changed
+ mMap.setOnMarkerDragListener(new GoogleMap.OnMarkerDragListener()
+ {
/**
* {@inheritDoc}
*/
@Override
- public void onMarkerDragStart(Marker marker) {}
+ public void onMarkerDragStart(Marker marker){}
/**
* {@inheritDoc}
* Updates the start position of the user.
*/
@Override
- public void onMarkerDragEnd(Marker marker) {
+ public void onMarkerDragEnd(Marker marker)
+ {
startPosition[0] = (float) marker.getPosition().latitude;
startPosition[1] = (float) marker.getPosition().longitude;
}
@@ -139,11 +152,10 @@ public void onMarkerDragEnd(Marker marker) {
* {@inheritDoc}
*/
@Override
- public void onMarkerDrag(Marker marker) {}
+ public void onMarkerDrag(Marker marker){}
});
}
});
-
return rootView;
}
@@ -154,8 +166,8 @@ public void onMarkerDrag(Marker marker) {}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
-
- this.button = view.findViewById(R.id.startLocationDone);
+ // Add button to begin PDR recording and go to recording fragment.
+ this.button = (Button) getView().findViewById(R.id.startLocationDone);
this.button.setOnClickListener(new View.OnClickListener() {
/**
* {@inheritDoc}
@@ -164,31 +176,16 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
*/
@Override
public void onClick(View view) {
- float chosenLat = startPosition[0];
- float chosenLon = startPosition[1];
-
- // If the Activity is RecordingActivity
- if (requireActivity() instanceof RecordingActivity) {
- // Start sensor recording + set the start location
- sensorFusion.startRecording();
- sensorFusion.setStartGNSSLatitude(startPosition);
-
- // Now switch to the recording screen
- ((RecordingActivity) requireActivity()).showRecordingScreen();
-
- // If the Activity is ReplayActivity
- } else if (requireActivity() instanceof ReplayActivity) {
- // *Do not* cast to RecordingActivity here
- // Just call the Replay method
- ((ReplayActivity) requireActivity()).onStartLocationChosen(chosenLat, chosenLon);
-
- // Otherwise (unexpected host)
- } else {
- // Optional: log or handle error
- // Log.e("StartLocationFragment", "Unknown host Activity: " + requireActivity());
- }
+ // Starts recording data from the sensor fusion
+ sensorFusion.startRecording();
+ // Set the start location obtained
+ sensorFusion.setStartGNSSLatitude(startPosition);
+ // Navigate to the RecordingFragment
+ NavDirections action = StartLocationFragmentDirections.actionStartLocationFragmentToRecordingFragment();
+ Navigation.findNavController(view).navigate(action);
}
});
+
}
/**
@@ -198,9 +195,10 @@ public void onClick(View view) {
*/
private void switchFloorNU(int floorIndex) {
FloorNK = floorIndex; // Set the current floor index
- if (nucleusBuildingManager != null) {
+ if (NucleusBuildingManager != null) {
// Call the switchFloor method of the IndoorMapManager to switch to the specified floor
- nucleusBuildingManager.getIndoorMapManager().switchFloor(floorIndex);
+ NucleusBuildingManager.getIndoorMapManager().switchFloor(floorIndex);
}
}
+
}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/UploadFragment.java b/app/src/main/java/com/openpositioning/PositionMe/fragments/UploadFragment.java
similarity index 76%
rename from app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/UploadFragment.java
rename to app/src/main/java/com/openpositioning/PositionMe/fragments/UploadFragment.java
index 9d435812..458371e9 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/UploadFragment.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/fragments/UploadFragment.java
@@ -1,12 +1,10 @@
-package com.openpositioning.PositionMe.presentation.fragment;
+package com.openpositioning.PositionMe.fragments;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
-import android.os.Environment;
-import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -15,17 +13,15 @@
import androidx.recyclerview.widget.RecyclerView;
import com.openpositioning.PositionMe.R;
-import com.openpositioning.PositionMe.data.remote.ServerCommunications;
-import com.openpositioning.PositionMe.presentation.viewitems.UploadViewHolder;
-import com.openpositioning.PositionMe.presentation.viewitems.DownloadClickListener;
-import com.openpositioning.PositionMe.presentation.viewitems.UploadListAdapter;
+import com.openpositioning.PositionMe.ServerCommunications;
+import com.openpositioning.PositionMe.viewitems.DownloadClickListener;
+import com.openpositioning.PositionMe.viewitems.UploadListAdapter;
import java.io.File;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
-
/**
* A simple {@link Fragment} subclass. Displays trajectories that were saved locally because no
* acceptable network was available to upload it when the recording finished. Trajectories can be
@@ -59,28 +55,13 @@ public UploadFragment() {
* Initialises new Server Communication instance with the context, and finds all the files that
* match the trajectory naming scheme in local storage.
*/
-
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Get communication class
serverCommunications = new ServerCommunications(getActivity());
-
- // Determine the directory to load trajectory files from.
- File trajectoriesDir = null;
-
- // for android 13 or higher use dedicated external storage
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- trajectoriesDir = getActivity().getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
- if (trajectoriesDir == null) {
- trajectoriesDir = getActivity().getFilesDir();
- }
- } else { // for android 12 or lower use internal storage
- trajectoriesDir = getActivity().getFilesDir();
- }
-
- localTrajectories = Stream.of(trajectoriesDir.listFiles((file, name) ->
- name.contains("trajectory_") && name.endsWith(".txt")))
+ // Load local trajectories
+ localTrajectories = Stream.of(getActivity().getFilesDir().listFiles((file, name) -> name.contains("trajectory_") && name.endsWith(".txt")))
.filter(file -> !file.isDirectory())
.collect(Collectors.toList());
}
@@ -107,8 +88,8 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container,
* is set up to upload the file when clicked and remove it from local storage.
*
* @see UploadListAdapter list adapter for the recycler view.
- * @see UploadViewHolder view holder for the recycler view.
- * @see com.openpositioning.PositionMe.R.layout#item_upload_card_view xml view for list elements.
+ * @see com.openpositioning.PositionMe.viewitems.UploadViewHolder view holder for the recycler view.
+ * @see R.layout#item_upload_card_view xml view for list elements.
*/
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
@@ -141,6 +122,11 @@ public void onPositionClicked(int position) {
// localTrajectories.remove(position);
// listAdapter.notifyItemRemoved(position);
}
+
+ @Override
+ public void onReplayClicked(int position) {
+ // 在上传界面不需要实现回放功能,但是需要提供一个空实现
+ }
});
uploadList.setAdapter(listAdapter);
}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/activity/MainActivity.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/activity/MainActivity.java
deleted file mode 100644
index 995f010d..00000000
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/activity/MainActivity.java
+++ /dev/null
@@ -1,366 +0,0 @@
-package com.openpositioning.PositionMe.presentation.activity;
-import android.Manifest;
-import android.content.SharedPreferences;
-
-import android.content.pm.PackageManager;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.widget.Toast;
-
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.app.AppCompatDelegate;
-import androidx.appcompat.widget.Toolbar;
-
-import androidx.core.content.ContextCompat;
-import androidx.navigation.NavController;
-import androidx.navigation.NavOptions;
-import androidx.navigation.fragment.NavHostFragment;
-import androidx.navigation.ui.AppBarConfiguration;
-import androidx.navigation.ui.NavigationUI;
-import androidx.preference.PreferenceManager;
-
-import com.openpositioning.PositionMe.R;
-import com.openpositioning.PositionMe.data.remote.ServerCommunications;
-import com.openpositioning.PositionMe.presentation.fragment.HomeFragment;
-import com.openpositioning.PositionMe.presentation.fragment.SettingsFragment;
-import com.openpositioning.PositionMe.sensors.Observer;
-import com.openpositioning.PositionMe.sensors.SensorFusion;
-import com.openpositioning.PositionMe.utils.PermissionManager;
-
-
-import java.util.Objects;
-
-/**
- * The Main Activity of the application, handling setup, permissions and starting all other fragments
- * and processes.
- * The Main Activity takes care of most essential tasks before the app can run. Such as setting up
- * the views, and enforcing light mode so the colour scheme is consistent. It initialises the
- * various fragments and the navigation between them, getting the Navigation controller. It also
- * loads the custom action bar with the set theme and icons, and enables back-navigation. The shared
- * preferences are also loaded.
- *
- * The most important task of the main activity is check and asking for the necessary permissions to
- * enable the application to use the required hardware devices. This is done through a number of
- * functions that call the OS, as well as pop-up messages warning the user if permissions are denied.
- *
- * Once all permissions are granted, the Main Activity obtains the Sensor Fusion instance and sets
- * the context, enabling the Fragments to interact with the class without setting it up again.
- *
- * @see HomeFragment the initial fragment displayed.
- * @see com.openpositioning.PositionMe.R.navigation the navigation graph.
- * @see SensorFusion the singletion data processing class.
- *
- * @author Mate Stodulka
- * @author Virginia Cangelosi
- */
-public class MainActivity extends AppCompatActivity implements Observer {
-
-
- //region Instance variables
- private NavController navController;
- private ActivityResultLauncher locationPermissionLauncher;
- private ActivityResultLauncher multiplePermissionsLauncher;
-
- private SharedPreferences settings;
- private SensorFusion sensorFusion;
- private Handler httpResponseHandler;
-
- private PermissionManager permissionManager;
-
- private static final int PERMISSION_REQUEST_CODE = 100;
-
- //endregion
-
- //region Activity Lifecycle
-
- /**
- * {@inheritDoc}
- * Forces light mode, sets up the navigation graph, initialises the toolbar with back action on
- * the nav controller, loads the shared preferences and checks for all permissions necessary.
- * Sets up a Handler for displaying messages from other classes.
- */
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
- setContentView(R.layout.activity_main);
-
- // Set up navigation and fragments
- NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager()
- .findFragmentById(R.id.nav_host_fragment);
- navController = Objects.requireNonNull(navHostFragment).getNavController();
-
- // Set action bar
- Toolbar toolbar = findViewById(R.id.main_toolbar);
- setSupportActionBar(toolbar);
- toolbar.showOverflowMenu();
- toolbar.setBackgroundColor(ContextCompat.getColor(getApplicationContext(), R.color.md_theme_light_surface));
- toolbar.setTitleTextColor(ContextCompat.getColor(getApplicationContext(), R.color.black));
- toolbar.setNavigationIcon(R.drawable.ic_baseline_back_arrow);
-
- // Set up back action with NavigationUI
- AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
- NavigationUI.setupWithNavController(toolbar, navController, appBarConfiguration);
-
- // Get handle for settings
- this.settings = PreferenceManager.getDefaultSharedPreferences(this);
- settings.edit().putBoolean("permanentDeny", false).apply();
-
- // Initialize SensorFusion early so that its context is set
- this.sensorFusion = SensorFusion.getInstance();
- this.sensorFusion.setContext(getApplicationContext());
-
- // Register multiple permissions launcher
- multiplePermissionsLauncher = registerForActivityResult(
- new ActivityResultContracts.RequestMultiplePermissions(),
- result -> {
- boolean locationGranted = result.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false);
- boolean activityGranted = result.getOrDefault(Manifest.permission.ACTIVITY_RECOGNITION, false);
-
- if (locationGranted && activityGranted) {
- // Both permissions granted
- allPermissionsObtained();
- } else {
- // Permission denied
- Toast.makeText(this,
- "Location or Physical Activity permission denied. Some features may not work.",
- Toast.LENGTH_LONG).show();
- }
- }
- );
-
- // Handler for global toasts and popups from other classes
- this.httpResponseHandler = new Handler();
- }
-
-
-
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void onPause() {
- super.onPause();
-
- //Ensure sensorFusion has been initialised before unregistering listeners
- if(sensorFusion != null) {
-// sensorFusion.stopListening();
- }
- }
-
- /**
- * {@inheritDoc}
- * Checks for activities in case the app was closed without granting them, or if they were
- * granted through the settings page. Repeats the startup checks done in
- * {@link MainActivity#onCreate(Bundle)}. Starts listening in the SensorFusion class.
- *
- * @see SensorFusion the main data processing class.
- */
- @Override
- public void onResume() {
- super.onResume();
-
- if (getSupportActionBar() != null) {
- getSupportActionBar().show();
- }
-
- // Delay permission check slightly to ensure the Activity is in the foreground
- new Handler().postDelayed(() -> {
- if (isActivityVisible()) {
- // Check if both permissions are granted
- boolean locationGranted = ContextCompat.checkSelfPermission(
- this, Manifest.permission.ACCESS_FINE_LOCATION
- ) == PackageManager.PERMISSION_GRANTED;
-
- boolean activityGranted = ContextCompat.checkSelfPermission(
- this, Manifest.permission.ACTIVITY_RECOGNITION
- ) == PackageManager.PERMISSION_GRANTED;
-
- if (!locationGranted || !activityGranted) {
- // Request both permissions using ActivityResultLauncher
- multiplePermissionsLauncher.launch(new String[]{
- Manifest.permission.ACCESS_FINE_LOCATION
- });
- multiplePermissionsLauncher.launch(new String[]{
- Manifest.permission.ACTIVITY_RECOGNITION
- });
- } else {
- // Both permissions are already granted
- allPermissionsObtained();
- }
- }
- }, 300); // Delay ensures activity is fully visible before requesting permissions
-
- if (sensorFusion != null) {
- sensorFusion.resumeListening();
- }
- }
-
- private boolean isActivityVisible() {
- return !isFinishing() && !isDestroyed();
- }
-
-
-
- /**
- * Unregisters sensor listeners when the app closes. Not in {@link MainActivity#onPause()} to
- * enable recording data with a locked screen.
- *
- * @see SensorFusion the main data processing class.
- */
- @Override
- protected void onDestroy() {
- if (sensorFusion != null) {
-// sensorFusion.stopListening(); // suspended due to the need to record data with
-// a locked screen or cross activity
- }
- super.onDestroy();
- }
-
-
- //endregion
-
- //region Permissions
-
- /**
- * Prepares global resources when all permissions are granted.
- * Resets the permissions tracking boolean in shared preferences, and initialises the
- * {@link SensorFusion} class with the application context, and registers the main activity to
- * listen for server responses that SensorFusion receives.
- *
- * @see SensorFusion the main data processing class.
- * @see ServerCommunications the communication class sending and recieving data from the server.
- */
- private void allPermissionsObtained() {
- // Reset any permission denial flag in SharedPreferences if needed.
- settings.edit().putBoolean("permanentDeny", false).apply();
-
- // Ensure SensorFusion is initialized with a valid context.
- if (this.sensorFusion == null) {
- this.sensorFusion = SensorFusion.getInstance();
- this.sensorFusion.setContext(getApplicationContext());
- }
- sensorFusion.registerForServerUpdate(this);
- }
-
-
-
-
- //endregion
-
- //region Navigation
-
- /**
- * {@inheritDoc}
- * Sets desired animations and navigates to {@link SettingsFragment}
- * when the settings wheel in the action bar is clicked.
- */
- @Override
- public boolean onOptionsItemSelected(@NonNull MenuItem item) {
- if(Objects.requireNonNull(navController.getCurrentDestination()).getId() == item.getItemId())
- return super.onOptionsItemSelected(item);
- else {
- NavOptions options = new NavOptions.Builder()
- .setLaunchSingleTop(true)
- .setEnterAnim(R.anim.slide_in_bottom)
- .setExitAnim(R.anim.slide_out_top)
- .setPopEnterAnim(R.anim.slide_in_top)
- .setPopExitAnim(R.anim.slide_out_bottom).build();
- navController.navigate(R.id.action_global_settingsFragment, null, options);
- return true;
- }
- }
-
- /**
- * {@inheritDoc}
- * Enables navigating back between fragments.
- */
- @Override
- public boolean onSupportNavigateUp() {
- return navController.navigateUp() || super.onSupportNavigateUp();
- }
-
- /**
- * {@inheritDoc}
- * Inflate the designed menu view.
- *
- * @see com.openpositioning.PositionMe.R.menu for the xml file.
- */
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- getMenuInflater().inflate(R.menu.menu_items, menu);
- return true;
- }
-
- /**
- * {@inheritDoc}
- * Handles the back button press. If the current fragment is the HomeFragment, a dialog is
- * displayed to confirm the exit. If not, the default back navigation is performed.
- */
- @Override
- public void onBackPressed() {
- // Check if the current destination is HomeFragment (assumed to be the root)
- if (navController.getCurrentDestination() != null &&
- navController.getCurrentDestination().getId() == R.id.homeFragment) {
- new AlertDialog.Builder(this)
- .setTitle("Confirm Exit")
- .setMessage("Are you sure you want to exit the app?")
- .setPositiveButton("Yes", (dialog, which) -> {
- dialog.dismiss();
- finish(); // Close the activity (exit the app)
- })
- .setNegativeButton("No", (dialog, which) -> dialog.dismiss())
- .create()
- .show();
- } else {
- // If not on the root destination, perform the default back navigation.
- super.onBackPressed();
- }
- }
-
-
-
- //endregion
-
- //region Global toasts
-
- /**
- * {@inheritDoc}
- * Calls the corresponding handler that runs a toast on the Main UI thread.
- */
- @Override
- public void update(Object[] objList) {
- assert objList[0] instanceof Boolean;
- if((Boolean) objList[0]) {
- this.httpResponseHandler.post(displayToastTaskSuccess);
- }
- else {
- this.httpResponseHandler.post(displayToastTaskFailure);
- }
- }
-
- /**
- * Task that displays positive toast on the main UI thread.
- * Called when {@link ServerCommunications} successfully uploads a trajectory.
- */
- private final Runnable displayToastTaskSuccess = () -> Toast.makeText(MainActivity.this,
- "Trajectory uploaded", Toast.LENGTH_SHORT).show();
-
- /**
- * Task that displays negative toast on the main UI thread.
- * Called when {@link ServerCommunications} fails to upload a trajectory.
- */
- private final Runnable displayToastTaskFailure = () -> {
-// Toast.makeText(MainActivity.this, "Failed to complete trajectory upload", Toast.LENGTH_SHORT).show();
- };
-
- //endregion
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/activity/RecordingActivity.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/activity/RecordingActivity.java
deleted file mode 100644
index c0d82ae2..00000000
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/activity/RecordingActivity.java
+++ /dev/null
@@ -1,92 +0,0 @@
-package com.openpositioning.PositionMe.presentation.activity;
-
-import android.os.Bundle;
-import android.view.WindowManager;
-
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.fragment.app.FragmentTransaction;
-
-import com.openpositioning.PositionMe.R;
-import com.openpositioning.PositionMe.presentation.fragment.StartLocationFragment;
-import com.openpositioning.PositionMe.presentation.fragment.RecordingFragment;
-import com.openpositioning.PositionMe.presentation.fragment.CorrectionFragment;
-
-
-/**
- * The RecordingActivity manages the recording flow of the application, guiding the user through a sequence
- * of screens for location selection, recording, and correction before finalizing the process.
- *
- * This activity follows a structured workflow:
- *
- * StartLocationFragment - Allows users to select their starting location.
- * RecordingFragment - Handles the recording process and contains a TrajectoryMapFragment.
- * CorrectionFragment - Enables users to review and correct recorded data before completion.
- *
- *
- * The activity ensures that the screen remains on during the recording process to prevent interruptions.
- * It also provides fragment transactions for seamless navigation between different stages of the workflow.
- *
- * This class is referenced in various fragments such as HomeFragment, StartLocationFragment,
- * RecordingFragment, and CorrectionFragment to control navigation through the recording flow.
- *
- * @see StartLocationFragment The first step in the recording process where users select their starting location.
- * @see RecordingFragment Handles data recording and map visualization.
- * @see CorrectionFragment Allows users to review and make corrections before finalizing the process.
- * @see com.openpositioning.PositionMe.R.layout#activity_recording The associated layout for this activity.
- *
- * @author ShuGu
- */
-
-public class RecordingActivity extends AppCompatActivity {
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_recording);
-
- if (savedInstanceState == null) {
- showStartLocationScreen(); // Start with the user selecting the start location
- }
-
- // Keep screen on
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- }
-
- /**
- * Show the StartLocationFragment (beginning of flow).
- */
- public void showStartLocationScreen() {
- FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
- ft.replace(R.id.mainFragmentContainer, new StartLocationFragment());
- ft.commit();
- }
-
- /**
- * Show the RecordingFragment, which contains the TrajectoryMapFragment internally.
- */
- public void showRecordingScreen() {
- FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
- ft.replace(R.id.mainFragmentContainer, new RecordingFragment());
- ft.addToBackStack(null);
- ft.commit();
- }
-
- /**
- * Show the CorrectionFragment after the user stops recording.
- */
- public void showCorrectionScreen() {
- FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
- ft.replace(R.id.mainFragmentContainer, new CorrectionFragment());
- ft.addToBackStack(null);
- ft.commit();
- }
-
- /**
- * Finish the Activity (or do any final steps) once corrections are done.
- */
- public void finishFlow() {
- getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- finish();
- }
-}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/activity/ReplayActivity.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/activity/ReplayActivity.java
deleted file mode 100644
index c6a30472..00000000
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/activity/ReplayActivity.java
+++ /dev/null
@@ -1,130 +0,0 @@
-package com.openpositioning.PositionMe.presentation.activity;
-
-import android.os.Bundle;
-import android.util.Log;
-import java.io.File;
-
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-
-import com.openpositioning.PositionMe.R;
-import com.openpositioning.PositionMe.presentation.fragment.ReplayFragment;
-import com.openpositioning.PositionMe.presentation.fragment.StartLocationFragment;
-
-
-/**
- * The ReplayActivity is responsible for managing the replay session of a user's trajectory.
- * It handles the process of retrieving the trajectory data, displaying relevant fragments, and
- * facilitating the interaction with the user to choose the starting location before displaying the
- * replay of the trajectory.
- *
- * The activity starts by extracting the trajectory file path from the intent that launched it. If
- * the file path is not provided or is empty, it uses a default file path. It ensures that the trajectory
- * file exists before proceeding. Once the file is verified, it shows the StartLocationFragment, which allows
- * the user to select their starting location (latitude and longitude). After the user has selected the
- * starting point, the activity switches to the ReplayFragment to display the replay of the user's trajectory.
- *
- * The activity also provides functionality to finish the replay session and exit the activity once the replay
- * process has completed.
- *
- * This activity makes use of a few key constants for passing data between fragments, including the trajectory file
- * path and the initial latitude and longitude. These constants are defined at the beginning of the class.
- *
- * The ReplayActivity manages the interaction between fragments by facilitating communication from the
- * StartLocationFragment to the ReplayFragment, where the replay of the trajectory is displayed.
- *
- * @see StartLocationFragment The fragment where the user selects their start location for the trajectory replay.
- * @see ReplayFragment The fragment responsible for showing the trajectory replay.
- *
- * @author Shu Gu
- */
-
-public class ReplayActivity extends AppCompatActivity {
-
- public static final String TAG = "ReplayActivity";
- public static final String EXTRA_INITIAL_LAT = "extra_initial_lat";
- public static final String EXTRA_INITIAL_LON = "extra_initial_lon";
- public static final String EXTRA_TRAJECTORY_FILE_PATH = "extra_trajectory_file_path";
-
- private String filePath;
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_replay);
- // Get the trajectory file path from the Intent
- filePath = getIntent().getStringExtra(EXTRA_TRAJECTORY_FILE_PATH);
-
- // Debug log: Received file path
- Log.i(TAG, "Received trajectory file path: " + filePath);
-
- if (filePath == null || filePath.isEmpty()) {
- // If not provided, set a default path (or show an error message)
- filePath = "/storage/emulated/0/Download/trajectory_default.txt";
- Log.e(TAG, "No trajectory file path provided, using default: " + filePath);
- }
-
- // Check if file exists before proceeding
- if (!new File(filePath).exists()) {
- Log.e(TAG, "Trajectory file does NOT exist: " + filePath);
- } else {
- Log.i(TAG, "Trajectory file exists: " + filePath);
- }
-
- // Show StartLocationFragment first to let user pick location
- if (savedInstanceState == null) {
- showStartLocationFragment();
- }
- }
-
- /**
- * Display a StartLocationFragment to let user set their start location.
- * Displays the ReplayFragment and passes the trajectory file path as an argument.
- */
- private void showStartLocationFragment() {
- Log.d(TAG, "Showing StartLocationFragment...");
- StartLocationFragment startLocationFragment = new StartLocationFragment();
- getSupportFragmentManager()
- .beginTransaction()
- .replace(R.id.replayActivityContainer, startLocationFragment)
- .commit();
- }
-
- /**
- * Called by StartLocationFragment when user picks their start location.
- */
- public void onStartLocationChosen(float lat, float lon) {
- Log.i(TAG, "User selected start location: Lat=" + lat + ", Lon=" + lon);
- showReplayFragment(filePath, lat, lon);
- }
-
- /**
- * Display ReplayFragment, passing file path and starting lat/lon as arguments.
- */
- public void showReplayFragment(String filePath, float initialLat, float initialLon) {
- Log.d(TAG, "Switching to ReplayFragment with file: " + filePath +
- ", Initial Lat: " + initialLat + ", Initial Lon: " + initialLon);
-
- ReplayFragment replayFragment = new ReplayFragment();
- // Pass the file path through a Bundle
- Bundle args = new Bundle();
- args.putString(EXTRA_TRAJECTORY_FILE_PATH, filePath);
- args.putFloat(EXTRA_INITIAL_LAT, initialLat);
- args.putFloat(EXTRA_INITIAL_LON, initialLon);
- replayFragment.setArguments(args);
-
- getSupportFragmentManager()
- .beginTransaction()
- .replace(R.id.replayActivityContainer, replayFragment)
- .commit();
- }
-
- /**
- * Finish replay session
- * Called when the replay process is completed.
- */
- public void finishFlow() {
- Log.d(TAG, "Replay session finished.");
- finish();
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/CorrectionFragment.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/CorrectionFragment.java
deleted file mode 100644
index 8f94cb27..00000000
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/CorrectionFragment.java
+++ /dev/null
@@ -1,161 +0,0 @@
-package com.openpositioning.PositionMe.presentation.fragment;
-
-import android.os.Bundle;
-import android.text.Editable;
-import android.text.TextWatcher;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.fragment.app.Fragment;
-
-import com.openpositioning.PositionMe.R;
-import com.openpositioning.PositionMe.presentation.activity.RecordingActivity;
-import com.openpositioning.PositionMe.sensors.SensorFusion;
-import com.openpositioning.PositionMe.utils.PathView;
-import com.google.android.gms.maps.CameraUpdateFactory;
-import com.google.android.gms.maps.GoogleMap;
-import com.google.android.gms.maps.OnMapReadyCallback;
-import com.google.android.gms.maps.SupportMapFragment;
-import com.google.android.gms.maps.model.LatLng;
-import com.google.android.gms.maps.model.MarkerOptions;
-
-/**
- * A simple {@link Fragment} subclass. Corrections Fragment is displayed after a recording session
- * is finished to enable manual adjustments to the PDR. The adjustments are not saved as of now.
- */
-public class CorrectionFragment extends Fragment {
-
- //Map variable
- public GoogleMap mMap;
- //Button to go to next
- private Button button;
- //Singleton SensorFusion class
- private SensorFusion sensorFusion = SensorFusion.getInstance();
- private TextView averageStepLengthText;
- private EditText stepLengthInput;
- private float averageStepLength;
- private float newStepLength;
- private int secondPass = 0;
- private CharSequence changedText;
- private static float scalingRatio = 0f;
- private static LatLng start;
- private PathView pathView;
-
- public CorrectionFragment() {
- // Required empty public constructor
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- AppCompatActivity activity = (AppCompatActivity) getActivity();
- if (activity != null && activity.getSupportActionBar() != null) {
- activity.getSupportActionBar().hide();
- }
- View rootView = inflater.inflate(R.layout.fragment_correction, container, false);
-
- // Send trajectory data to the cloud
- sensorFusion.sendTrajectoryToCloud();
-
- //Obtain start position
- float[] startPosition = sensorFusion.getGNSSLatitude(true);
-
- // Initialize map fragment
- SupportMapFragment supportMapFragment=(SupportMapFragment)
- getChildFragmentManager().findFragmentById(R.id.map);
-
- supportMapFragment.getMapAsync(new OnMapReadyCallback() {
- @Override
- public void onMapReady(GoogleMap map) {
- mMap = map;
- mMap.setMapType(GoogleMap.MAP_TYPE_HYBRID);
- mMap.getUiSettings().setCompassEnabled(true);
- mMap.getUiSettings().setTiltGesturesEnabled(true);
- mMap.getUiSettings().setRotateGesturesEnabled(true);
- mMap.getUiSettings().setScrollGesturesEnabled(true);
-
- // Add a marker at the start position
- start = new LatLng(startPosition[0], startPosition[1]);
- mMap.addMarker(new MarkerOptions().position(start).title("Start Position"));
-
- // Calculate zoom for demonstration
- double zoom = Math.log(156543.03392f * Math.cos(startPosition[0] * Math.PI / 180)
- * scalingRatio) / Math.log(2);
- mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(start, (float) zoom));
- }
- });
-
- return rootView;
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
-
- this.averageStepLengthText = view.findViewById(R.id.averageStepView);
- this.stepLengthInput = view.findViewById(R.id.inputStepLength);
- this.pathView = view.findViewById(R.id.pathView1);
-
- averageStepLength = sensorFusion.passAverageStepLength();
- averageStepLengthText.setText(getString(R.string.averageStepLgn) + ": "
- + String.format("%.2f", averageStepLength));
-
- // Listen for ENTER key
- this.stepLengthInput.setOnKeyListener((v, keyCode, event) -> {
- if (keyCode == KeyEvent.KEYCODE_ENTER) {
- newStepLength = Float.parseFloat(changedText.toString());
- // Rescale path
- sensorFusion.redrawPath(newStepLength / averageStepLength);
- averageStepLengthText.setText(getString(R.string.averageStepLgn)
- + ": " + String.format("%.2f", newStepLength));
- pathView.invalidate();
-
- secondPass++;
- if (secondPass == 2) {
- averageStepLength = newStepLength;
- secondPass = 0;
- }
- }
- return false;
- });
-
- this.stepLengthInput.addTextChangedListener(new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count,int after) {}
- @Override
- public void onTextChanged(CharSequence s, int start, int before,int count) {}
- @Override
- public void afterTextChanged(Editable s) {
- changedText = s;
- }
- });
-
- // Button to finalize corrections
- this.button = view.findViewById(R.id.correction_done);
- this.button.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- // ************* CHANGED CODE HERE *************
- // Before:
- // NavDirections action = CorrectionFragmentDirections.actionCorrectionFragmentToHomeFragment();
- // Navigation.findNavController(view).navigate(action);
- // ((AppCompatActivity)getActivity()).getSupportActionBar().show();
-
- // Now, simply tell the Activity we are done:
- ((RecordingActivity) requireActivity()).finishFlow();
- }
- });
- }
-
- public void setScalingRatio(float scalingRatio) {
- this.scalingRatio = scalingRatio;
- }
-}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/FilesFragment.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/FilesFragment.java
deleted file mode 100644
index 83bc4ef1..00000000
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/FilesFragment.java
+++ /dev/null
@@ -1,221 +0,0 @@
-package com.openpositioning.PositionMe.presentation.fragment;
-
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.cardview.widget.CardView;
-import androidx.fragment.app.Fragment;
-import androidx.navigation.NavDirections;
-import androidx.navigation.Navigation;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.openpositioning.PositionMe.R;
-import com.openpositioning.PositionMe.data.remote.ServerCommunications;
-import com.openpositioning.PositionMe.presentation.viewitems.TrajDownloadViewHolder;
-import com.openpositioning.PositionMe.sensors.Observer;
-import com.openpositioning.PositionMe.presentation.viewitems.TrajDownloadListAdapter;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * A simple {@link Fragment} subclass. The files fragments displays a list of trajectories already
- * uploaded with some metadata, and enabled re-downloading them to the device's local storage.
- *
- * @see HomeFragment the connected fragment in the nav graph.
- * @see UploadFragment sub-menu for uploading recordings that failed during recording.
- * @see com.openpositioning.PositionMe.Traj the data structure sent and received.
- * @see ServerCommunications the class handling communication with the server.
- *
- * @author Mate Stodulka
- */
-public class FilesFragment extends Fragment implements Observer {
-
- // UI elements
- private RecyclerView filesList;
- private TrajDownloadListAdapter listAdapter;
- private CardView uploadCard;
-
- // Class handling HTTP communication
- private ServerCommunications serverCommunications;
-
- /**
- * Default public constructor, empty.
- */
- public FilesFragment() {
- // Required empty public constructor
- }
-
- /**
- * {@inheritDoc}
- * Initialise the server communication class and register the FilesFragment as an Observer to
- * receive the async http responses.
- */
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- serverCommunications = new ServerCommunications(getActivity());
- serverCommunications.registerObserver(this);
- }
-
- /**
- * {@inheritDoc}
- * Sets the title in the action bar.
- */
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- // Inflate the layout for this fragment
- View rootView = inflater.inflate(R.layout.fragment_files, container, false);
- getActivity().setTitle("Trajectory recordings");
- return rootView;
- }
-
- /**
- * {@inheritDoc}
- * Initialises UI elements, including a navigation card to the {@link UploadFragment} and a
- * RecyclerView displaying online trajectories.
- *
- * @see TrajDownloadViewHolder the View Holder for the list.
- * @see TrajDownloadListAdapter the list adapter for displaying the recycler view.
- * @see com.openpositioning.PositionMe.R.layout#item_trajectorycard_view the elements in the list.
- */
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- // Get recyclerview
- filesList = view.findViewById(R.id.filesList);
- // Get clickable card view
- uploadCard = view.findViewById(R.id.uploadCard);
- uploadCard.setOnClickListener(new View.OnClickListener() {
- /**
- * {@inheritDoc}
- * Navigates to {@link UploadFragment}.
- */
- @Override
- public void onClick(View view) {
- NavDirections action = FilesFragmentDirections.actionFilesFragmentToUploadFragment();
- Navigation.findNavController(view).navigate(action);
- }
- });
- // Request list of uploaded trajectories from the server.
- serverCommunications.sendInfoRequest();
- // Force RecyclerView refresh to ensure icon states are correct
- new Handler(Looper.getMainLooper()).postDelayed(() -> {
- if (filesList.getAdapter() != null) {
- filesList.getAdapter().notifyDataSetChanged();
- System.out.println("RecyclerView refreshed after page load.");
- }
- }, 500);
- }
-
- /**
- * {@inheritDoc}
- * Called by {@link ServerCommunications} when the response to the HTTP info request is received.
- *
- * @param singletonStringList a single string wrapped in an object array containing the http
- * response from the server.
- */
- @Override
- public void update(Object[] singletonStringList) {
- // Cast input as a string
- String infoString = (String) singletonStringList[0];
- // Check if the string is non-null and non-empty before processing
- if(infoString != null && !infoString.isEmpty()) {
- // Process string
- List> entryList = processInfoResponse(infoString);
- // Start a handler to be able to modify UI elements
- new Handler(Looper.getMainLooper()).post(new Runnable() {
- @Override
- public void run() {
- // Update the RecyclerView with data from the server
- updateView(entryList);
- }
- });
- }
- }
-
- /**
- * Parses the info response string from the HTTP communication.
- * Process the data using the Json library and return the matching Java data structure as a
- * List of Maps of \. Throws a JSONException if the data is not valid.
- *
- * @param infoString HTTP info request response as a single string
- * @return List of Maps of String to String containing ID, owner ID, and date.
- */
- private List> processInfoResponse(String infoString) {
- // Initialise empty list
- List> entryList = new ArrayList<>();
- try {
- // Attempt to decode using known JSON pattern
- JSONArray jsonArray = new JSONArray(infoString);
- for (int i = 0; i < jsonArray.length(); i++) {
- JSONObject trajectoryEntry = jsonArray.getJSONObject(i);
- Map entryMap = new HashMap<>();
- entryMap.put("owner_id", String.valueOf(trajectoryEntry.get("owner_id")));
- entryMap.put("date_submitted", (String) trajectoryEntry.get("date_submitted"));
- entryMap.put("id", String.valueOf(trajectoryEntry.get("id")));
- // Add decoded map to list of entries
- entryList.add(entryMap);
- }
- } catch (JSONException e) {
- System.err.println("JSON reading failed");
- e.printStackTrace();
- }
- // Sort the list by the ID fields of the maps
- entryList.sort(Comparator.comparing(m -> Integer.parseInt(m.get("id")), Comparator.nullsLast(Comparator.naturalOrder())));
- return entryList;
- }
-
- /**
- * Update the RecyclerView in the FilesFragment with new data.
- * Must be called from a UI thread. Initialises a new Layout Manager, and passes it to the
- * RecyclerView. Initialises a {@link TrajDownloadListAdapter} with the input array and setting
- * up a listener so that trajectories are downloaded when clicked, and a pop-up message is
- * displayed to notify the user.
- *
- * @param entryList List of Maps of String to String containing metadata about the uploaded
- * trajectories (ID, owner ID, date).
- */
- private void updateView(List> entryList) {
- // Initialise RecyclerView with Manager and Adapter
- LinearLayoutManager manager = new LinearLayoutManager(getActivity());
- filesList.setLayoutManager(manager);
- filesList.setHasFixedSize(true);
- listAdapter = new TrajDownloadListAdapter(getActivity(), entryList, position -> {
- Map selectedItem = entryList.get(position);
- String id = selectedItem.get("id");
- String dateSubmitted = selectedItem.get("date_submitted");
-
- // Pass ID and date_submitted
- serverCommunications.downloadTrajectory(position, id, dateSubmitted);
-
-// new AlertDialog.Builder(getContext())
-// .setTitle("File downloaded")
-// .setMessage("Trajectory downloaded to local storage")
-// .setPositiveButton(R.string.ok, null)
-// .setNegativeButton(R.string.show_storage, (dialogInterface, i) -> {
-// startActivity(new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS));
-// })
-// .setIcon(R.drawable.ic_baseline_download_24)
-// .show();
- });
- filesList.setAdapter(listAdapter);
- // Force refresh RecyclerView to ensure downloadRecords changes are detected
- listAdapter.notifyDataSetChanged();
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/HomeFragment.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/HomeFragment.java
deleted file mode 100644
index 8371b04e..00000000
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/HomeFragment.java
+++ /dev/null
@@ -1,218 +0,0 @@
-package com.openpositioning.PositionMe.presentation.fragment;
-
-import android.Manifest;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.location.LocationManager;
-import android.os.Bundle;
-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.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.app.ActivityCompat;
-import androidx.fragment.app.Fragment;
-import androidx.navigation.NavDirections;
-import androidx.navigation.Navigation;
-import androidx.preference.PreferenceManager;
-
-import com.google.android.material.button.MaterialButton;
-import com.google.android.gms.maps.CameraUpdateFactory;
-import com.google.android.gms.maps.GoogleMap;
-import com.google.android.gms.maps.OnMapReadyCallback;
-import com.google.android.gms.maps.SupportMapFragment;
-import com.google.android.gms.maps.model.LatLng;
-import com.google.android.gms.maps.model.MarkerOptions;
-import com.openpositioning.PositionMe.R;
-import com.openpositioning.PositionMe.presentation.activity.RecordingActivity;
-
-/**
- * A simple {@link Fragment} subclass. The home fragment is the start screen of the application.
- * The home fragment acts as a hub for all other fragments, with buttons and icons for navigation.
- * The default screen when opening the application
- *
- * @see RecordingFragment
- * @see FilesFragment
- * @see MeasurementsFragment
- * @see SettingsFragment
- *
- * @author Mate Stodulka
- */
-public class HomeFragment extends Fragment implements OnMapReadyCallback {
-
- // Interactive UI elements to navigate to other fragments
- private MaterialButton goToInfo;
- private Button start;
- private Button measurements;
- private Button files;
- private TextView gnssStatusTextView;
-
- // For the map
- private GoogleMap mMap;
- private SupportMapFragment mapFragment;
-
- public HomeFragment() {
- // Required empty public constructor
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- }
-
- /**
- * {@inheritDoc}
- * Ensure the action bar is shown at the top of the screen. Set the title visible to Home.
- */
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- ((AppCompatActivity) getActivity()).getSupportActionBar().show();
- View rootView = inflater.inflate(R.layout.fragment_home, container, false);
- getActivity().setTitle("Home");
- return rootView;
- }
-
- /**
- * Initialise UI elements and set onClick actions for the buttons.
- */
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
-
- // Sensor Info button
- goToInfo = view.findViewById(R.id.sensorInfoButton);
- goToInfo.setOnClickListener(v -> {
- NavDirections action = HomeFragmentDirections.actionHomeFragmentToInfoFragment();
- Navigation.findNavController(v).navigate(action);
- });
-
- // Start/Stop Recording button
- start = view.findViewById(R.id.startStopButton);
- start.setEnabled(!PreferenceManager.getDefaultSharedPreferences(getContext())
- .getBoolean("permanentDeny", false));
- start.setOnClickListener(v -> {
- Intent intent = new Intent(requireContext(), RecordingActivity.class);
- startActivity(intent);
- ((AppCompatActivity) getActivity()).getSupportActionBar().hide();
- });
-
- // Measurements button
- measurements = view.findViewById(R.id.measurementButton);
- measurements.setOnClickListener(v -> {
- NavDirections action = HomeFragmentDirections.actionHomeFragmentToMeasurementsFragment();
- Navigation.findNavController(v).navigate(action);
- });
-
- // Files button
- files = view.findViewById(R.id.filesButton);
- files.setOnClickListener(v -> {
- NavDirections action = HomeFragmentDirections.actionHomeFragmentToFilesFragment();
- Navigation.findNavController(v).navigate(action);
- });
-
- // TextView to display GNSS disabled message
- gnssStatusTextView = view.findViewById(R.id.gnssStatusTextView);
-
- // Locate the MapFragment nested in this fragment
- mapFragment = (SupportMapFragment)
- getChildFragmentManager().findFragmentById(R.id.mapFragmentContainer);
- if (mapFragment != null) {
- // Asynchronously initialize the map
- mapFragment.getMapAsync(this);
- }
- }
-
- /**
- * Callback triggered when the Google Map is ready to be used.
- */
- @Override
- public void onMapReady(@NonNull GoogleMap googleMap) {
- mMap = googleMap;
- checkAndUpdatePermissions();
- }
-
- @Override
- public void onResume() {
- super.onResume();
- checkAndUpdatePermissions();
- }
-
- /**
- * Checks if GNSS/Location is enabled on the device.
- */
- private boolean isGnssEnabled() {
- LocationManager locationManager =
- (LocationManager) requireContext().getSystemService(Context.LOCATION_SERVICE);
- // Checks both GPS and network provider. Adjust as needed.
- boolean gpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
- boolean networkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
- return (gpsEnabled || networkEnabled);
- }
-
- /**
- * Move the map to the University of Edinburgh and display a message.
- */
- private void showEdinburghAndMessage(String message) {
- gnssStatusTextView.setText(message);
- gnssStatusTextView.setVisibility(View.VISIBLE);
-
- LatLng edinburghLatLng = new LatLng(55.944425, -3.188396);
- mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(edinburghLatLng, 15f));
- mMap.addMarker(new MarkerOptions()
- .position(edinburghLatLng)
- .title("University of Edinburgh"));
- }
-
- private void checkAndUpdatePermissions() {
-
- if (mMap == null) {
- return;
- }
-
- // Check if GNSS/Location is enabled
- boolean gnssEnabled = isGnssEnabled();
- if (gnssEnabled) {
- // Hide the "GNSS Disabled" message
- gnssStatusTextView.setVisibility(View.GONE);
-
- // Check runtime permissions for location
- if (ActivityCompat.checkSelfPermission(
- requireContext(), Manifest.permission.ACCESS_FINE_LOCATION)
- == PackageManager.PERMISSION_GRANTED ||
- ActivityCompat.checkSelfPermission(
- requireContext(), Manifest.permission.ACCESS_COARSE_LOCATION)
- == PackageManager.PERMISSION_GRANTED) {
-
- // Enable the MyLocation layer of Google Map
- mMap.setMyLocationEnabled(true);
-
- // Optionally move the camera to last known or default location:
- // (You could retrieve it from FusedLocationProvider or similar).
- // Here, just leaving it on default.
- // If you want to center on the user as soon as it loads, do something like:
- /*
- FusedLocationProviderClient fusedLocationClient =
- LocationServices.getFusedLocationProviderClient(requireContext());
- fusedLocationClient.getLastLocation().addOnSuccessListener(location -> {
- if (location != null) {
- LatLng currentLatLng = new LatLng(location.getLatitude(), location.getLongitude());
- mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(currentLatLng, 15f));
- }
- });
- */
- } else {
- // If no permission, simply show a default location or prompt for permissions
- showEdinburghAndMessage("Permission not granted. Please enable in settings.");
- }
- } else {
- // If GNSS is disabled, show University of Edinburgh + message
- showEdinburghAndMessage("GNSS is disabled. Please enable in settings.");
- }
- }
-}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/MeasurementsFragment.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/MeasurementsFragment.java
deleted file mode 100644
index 20c43987..00000000
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/MeasurementsFragment.java
+++ /dev/null
@@ -1,177 +0,0 @@
-package com.openpositioning.PositionMe.presentation.fragment;
-
-import android.os.Bundle;
-import android.os.Handler;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.cardview.widget.CardView;
-import androidx.constraintlayout.widget.ConstraintLayout;
-import androidx.fragment.app.Fragment;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.openpositioning.PositionMe.R;
-import com.openpositioning.PositionMe.sensors.SensorFusion;
-import com.openpositioning.PositionMe.sensors.SensorTypes;
-import com.openpositioning.PositionMe.sensors.Wifi;
-import com.openpositioning.PositionMe.presentation.viewitems.WifiListAdapter;
-
-import java.util.List;
-import java.util.Map;
-
-/**
- * A simple {@link Fragment} subclass. The measurement fragment displays the set of current sensor
- * readings. The values are refreshed periodically, but slower than their internal refresh rate.
- * The refresh time is set by a static constant.
- *
- * @see HomeFragment the previous fragment in the nav graph.
- * @see SensorFusion the source of all sensor readings.
- *
- * @author Mate Stodulka
- */
-public class MeasurementsFragment extends Fragment {
-
- // Static constant for refresh time in milliseconds
- private static final long REFRESH_TIME = 5000;
-
- // Singleton Sensor Fusion class handling all sensor data
- private SensorFusion sensorFusion;
-
- // UI Handler
- private Handler refreshDataHandler;
- // UI elements
- private ConstraintLayout sensorMeasurementList;
- private RecyclerView wifiListView;
- // List of string resource IDs
- private int[] prefaces;
- private int[] gnssPrefaces;
-
-
- /**
- * Public default constructor, empty.
- */
- public MeasurementsFragment() {
- // Required empty public constructor
- }
-
- /**
- * {@inheritDoc}
- * Obtains the singleton Sensor Fusion instance and initialises the string prefaces for display.
- * Creates a new handler to periodically refresh data.
- *
- * @see SensorFusion handles all sensor data.
- */
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- // Get sensor fusion instance
- sensorFusion = SensorFusion.getInstance();
- // Initialise string prefaces for display
- prefaces = new int[]{R.string.x, R.string.y, R.string.z};
- gnssPrefaces = new int[]{R.string.lati, R.string.longi};
-
- // Create new handler to refresh the UI.
- this.refreshDataHandler = new Handler();
- }
-
- /**
- * {@inheritDoc}
- * Sets title in the action bar to Sensor Measurements.
- * Posts the {@link MeasurementsFragment#refreshTableTask} using the Handler.
- */
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- // Inflate the layout for this fragment
- View rootView = inflater.inflate(R.layout.fragment_measurements, container, false);
- getActivity().setTitle("Sensor Measurements");
- this.refreshDataHandler.post(refreshTableTask);
- return rootView;
- }
-
- /**
- * {@inheritDoc}
- * Pauses the data refreshing when the fragment is not in focus.
- */
- @Override
- public void onPause() {
- refreshDataHandler.removeCallbacks(refreshTableTask);
- super.onPause();
- }
-
- /**
- * {@inheritDoc}
- * Restarts the data refresh when the fragment returns to focus.
- */
- @Override
- public void onResume() {
- refreshDataHandler.postDelayed(refreshTableTask, REFRESH_TIME);
- super.onResume();
- }
-
- /**
- * {@inheritDoc}
- * Obtains the constraint layout holding the sensor measurement values. Initialises the Recycler
- * View for holding WiFi data and registers its Layout Manager.
- */
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- sensorMeasurementList = (ConstraintLayout) getView().findViewById(R.id.sensorMeasurementList);
- wifiListView = (RecyclerView) getView().findViewById(R.id.wifiList);
- wifiListView.setLayoutManager(new LinearLayoutManager(getActivity()));
- }
-
- /**
- * Runnable task containing functionality to update the UI with the relevant sensor data.
- * Must be run on the UI thread via a Handler. Obtains movement sensor values and the current
- * WiFi networks from the {@link SensorFusion} instance and updates the UI with the new data
- * and the string wrappers provided.
- *
- * @see SensorFusion class handling all sensors and data processing.
- * @see Wifi class holding network data.
- */
- private final Runnable refreshTableTask = new Runnable() {
- @Override
- public void run() {
- // Get all the values from SensorFusion
- Map sensorValueMap = sensorFusion.getSensorValueMap();
- // Loop through UI elements and update the values
- for(SensorTypes st : SensorTypes.values()) {
- CardView cardView = (CardView) sensorMeasurementList.getChildAt(st.ordinal());
- ConstraintLayout currentRow = (ConstraintLayout) cardView.getChildAt(0);
- float[] values = sensorValueMap.get(st);
- for (int i = 0; i < values.length; i++) {
- String valueString;
- // Set string wrapper based on data type.
- if(values.length == 1) {
- valueString = getString(R.string.level, String.format("%.2f", values[0]));
- }
- else if(values.length == 2){
- if(st == SensorTypes.GNSSLATLONG)
- valueString = getString(gnssPrefaces[i], String.format("%.2f", values[i]));
- else
- valueString = getString(prefaces[i], String.format("%.2f", values[i]));
- }
- else{
- valueString = getString(prefaces[i], String.format("%.2f", values[i]));
- }
- ((TextView) currentRow.getChildAt(i + 1)).setText(valueString);
- }
- }
- // Get all WiFi values - convert to list of strings
- List wifiObjects = sensorFusion.getWifiList();
- // If there are WiFi networks visible, update the recycler view with the data.
- if(wifiObjects != null) {
- wifiListView.setAdapter(new WifiListAdapter(getActivity(), wifiObjects));
- }
- // Restart the data updater task in REFRESH_TIME milliseconds.
- refreshDataHandler.postDelayed(refreshTableTask, REFRESH_TIME);
- }
- };
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/RecordingFragment.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/RecordingFragment.java
deleted file mode 100644
index 6362a971..00000000
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/RecordingFragment.java
+++ /dev/null
@@ -1,298 +0,0 @@
-package com.openpositioning.PositionMe.presentation.fragment;
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.graphics.Color;
-import android.os.Bundle;
-import android.os.CountDownTimer;
-import android.os.Handler;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import android.view.animation.AlphaAnimation;
-import android.view.animation.Animation;
-import android.view.animation.LinearInterpolator;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.ProgressBar;
-import android.widget.TextView;
-import com.google.android.material.button.MaterialButton;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.preference.PreferenceManager;
-
-import com.openpositioning.PositionMe.R;
-import com.openpositioning.PositionMe.presentation.activity.RecordingActivity;
-import com.openpositioning.PositionMe.sensors.SensorFusion;
-import com.openpositioning.PositionMe.sensors.SensorTypes;
-import com.openpositioning.PositionMe.utils.UtilFunctions;
-import com.google.android.gms.maps.model.LatLng;
-
-
-/**
- * Fragment responsible for managing the recording process of trajectory data.
- *
- * The RecordingFragment serves as the interface for users to initiate, monitor, and
- * complete trajectory recording. It integrates sensor fusion data to track user movement
- * and updates a map view in real time. Additionally, it provides UI controls to cancel,
- * stop, and monitor recording progress.
- *
- * Features:
- * - Starts and stops trajectory recording.
- * - Displays real-time sensor data such as elevation and distance traveled.
- * - Provides UI controls to cancel or complete recording.
- * - Uses {@link TrajectoryMapFragment} to visualize recorded paths.
- * - Manages GNSS tracking and error display.
- *
- * @see TrajectoryMapFragment The map fragment displaying the recorded trajectory.
- * @see RecordingActivity The activity managing the recording workflow.
- * @see SensorFusion Handles sensor data collection.
- * @see SensorTypes Enumeration of available sensor types.
- *
- * @author Shu Gu
- */
-
-public class RecordingFragment extends Fragment {
-
- // UI elements
- private MaterialButton completeButton, cancelButton;
- private ImageView recIcon;
- private ProgressBar timeRemaining;
- private TextView elevation, distanceTravelled, gnssError;
-
- // App settings
- private SharedPreferences settings;
-
- // Sensor & data logic
- private SensorFusion sensorFusion;
- private Handler refreshDataHandler;
- private CountDownTimer autoStop;
-
- // Distance tracking
- private float distance = 0f;
- private float previousPosX = 0f;
- private float previousPosY = 0f;
-
- // References to the child map fragment
- private TrajectoryMapFragment trajectoryMapFragment;
-
- private final Runnable refreshDataTask = new Runnable() {
- @Override
- public void run() {
- updateUIandPosition();
- // Loop again
- refreshDataHandler.postDelayed(refreshDataTask, 200);
- }
- };
-
- public RecordingFragment() {
- // Required empty public constructor
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- this.sensorFusion = SensorFusion.getInstance();
- Context context = requireActivity();
- this.settings = PreferenceManager.getDefaultSharedPreferences(context);
- this.refreshDataHandler = new Handler();
- }
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater,
- @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- // Inflate only the "recording" UI parts (no map)
- return inflater.inflate(R.layout.fragment_recording, container, false);
- }
-
- @Override
- public void onViewCreated(@NonNull View view,
- @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
-
- // Child Fragment: the container in fragment_recording.xml
- // where TrajectoryMapFragment is placed
- trajectoryMapFragment = (TrajectoryMapFragment)
- getChildFragmentManager().findFragmentById(R.id.trajectoryMapFragmentContainer);
-
- // If not present, create it
- if (trajectoryMapFragment == null) {
- trajectoryMapFragment = new TrajectoryMapFragment();
- getChildFragmentManager()
- .beginTransaction()
- .replace(R.id.trajectoryMapFragmentContainer, trajectoryMapFragment)
- .commit();
- }
-
- // Initialize UI references
- elevation = view.findViewById(R.id.currentElevation);
- distanceTravelled = view.findViewById(R.id.currentDistanceTraveled);
- gnssError = view.findViewById(R.id.gnssError);
-
- completeButton = view.findViewById(R.id.stopButton);
- cancelButton = view.findViewById(R.id.cancelButton);
- recIcon = view.findViewById(R.id.redDot);
- timeRemaining = view.findViewById(R.id.timeRemainingBar);
-
- // Hide or initialize default values
- gnssError.setVisibility(View.GONE);
- elevation.setText(getString(R.string.elevation, "0"));
- distanceTravelled.setText(getString(R.string.meter, "0"));
-
- // Buttons
- completeButton.setOnClickListener(v -> {
- // Stop recording & go to correction
- if (autoStop != null) autoStop.cancel();
- sensorFusion.stopRecording();
- // Show Correction screen
- ((RecordingActivity) requireActivity()).showCorrectionScreen();
- });
-
-
- // Cancel button with confirmation dialog
- cancelButton.setOnClickListener(v -> {
- AlertDialog dialog = new AlertDialog.Builder(requireActivity())
- .setTitle("Confirm Cancel")
- .setMessage("Are you sure you want to cancel the recording? Your progress will be lost permanently!")
- .setNegativeButton("Yes", (dialogInterface, which) -> {
- // User confirmed cancellation
- sensorFusion.stopRecording();
- if (autoStop != null) autoStop.cancel();
- requireActivity().onBackPressed();
- })
- .setPositiveButton("No", (dialogInterface, which) -> {
- // User cancelled the dialog. Do nothing.
- dialogInterface.dismiss();
- })
- .create(); // Create the dialog but do not show it yet
-
- // Show the dialog and change the button color
- dialog.setOnShowListener(dialogInterface -> {
- Button negativeButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE);
- negativeButton.setTextColor(Color.RED); // Set "Yes" button color to red
- });
-
- dialog.show(); // Finally, show the dialog
- });
-
- // The blinking effect for recIcon
- blinkingRecordingIcon();
-
- // Start the timed or indefinite UI refresh
- if (this.settings.getBoolean("split_trajectory", false)) {
- // A maximum recording time is set
- long limit = this.settings.getInt("split_duration", 30) * 60000L;
- timeRemaining.setMax((int) (limit / 1000));
- timeRemaining.setProgress(0);
- timeRemaining.setScaleY(3f);
-
- autoStop = new CountDownTimer(limit, 1000) {
- @Override
- public void onTick(long millisUntilFinished) {
- timeRemaining.incrementProgressBy(1);
- updateUIandPosition();
- }
-
- @Override
- public void onFinish() {
- sensorFusion.stopRecording();
- ((RecordingActivity) requireActivity()).showCorrectionScreen();
- }
- }.start();
- } else {
- // No set time limit, just keep refreshing
- refreshDataHandler.post(refreshDataTask);
- }
- }
-
- /**
- * Update the UI with sensor data and pass map updates to TrajectoryMapFragment.
- */
- private void updateUIandPosition() {
- float[] pdrValues = sensorFusion.getSensorValueMap().get(SensorTypes.PDR);
- if (pdrValues == null) return;
-
- // Distance
- distance += Math.sqrt(Math.pow(pdrValues[0] - previousPosX, 2)
- + Math.pow(pdrValues[1] - previousPosY, 2));
- distanceTravelled.setText(getString(R.string.meter, String.format("%.2f", distance)));
-
- // Elevation
- float elevationVal = sensorFusion.getElevation();
- elevation.setText(getString(R.string.elevation, String.format("%.1f", elevationVal)));
-
- // Current location
- // Convert PDR coordinates to actual LatLng if you have a known starting lat/lon
- // Or simply pass relative data for the TrajectoryMapFragment to handle
- // For example:
- float[] latLngArray = sensorFusion.getGNSSLatitude(true);
- if (latLngArray != null) {
- LatLng oldLocation = trajectoryMapFragment.getCurrentLocation(); // or store locally
- LatLng newLocation = UtilFunctions.calculateNewPos(
- oldLocation == null ? new LatLng(latLngArray[0], latLngArray[1]) : oldLocation,
- new float[]{ pdrValues[0] - previousPosX, pdrValues[1] - previousPosY }
- );
-
- // Pass the location + orientation to the map
- if (trajectoryMapFragment != null) {
- trajectoryMapFragment.updateUserLocation(newLocation,
- (float) Math.toDegrees(sensorFusion.passOrientation()));
- }
- }
-
- // GNSS logic if you want to show GNSS error, etc.
- float[] gnss = sensorFusion.getSensorValueMap().get(SensorTypes.GNSSLATLONG);
- if (gnss != null && trajectoryMapFragment != null) {
- // If user toggles showing GNSS in the map, call e.g.
- if (trajectoryMapFragment.isGnssEnabled()) {
- LatLng gnssLocation = new LatLng(gnss[0], gnss[1]);
- LatLng currentLoc = trajectoryMapFragment.getCurrentLocation();
- if (currentLoc != null) {
- double errorDist = UtilFunctions.distanceBetweenPoints(currentLoc, gnssLocation);
- gnssError.setVisibility(View.VISIBLE);
- gnssError.setText(String.format(getString(R.string.gnss_error) + "%.2fm", errorDist));
- }
- trajectoryMapFragment.updateGNSS(gnssLocation);
- } else {
- gnssError.setVisibility(View.GONE);
- trajectoryMapFragment.clearGNSS();
- }
- }
-
- // Update previous
- previousPosX = pdrValues[0];
- previousPosY = pdrValues[1];
- }
-
- /**
- * Start the blinking effect for the recording icon.
- */
- private void blinkingRecordingIcon() {
- Animation blinking = new AlphaAnimation(1, 0);
- blinking.setDuration(800);
- blinking.setInterpolator(new LinearInterpolator());
- blinking.setRepeatCount(Animation.INFINITE);
- blinking.setRepeatMode(Animation.REVERSE);
- recIcon.startAnimation(blinking);
- }
-
- @Override
- public void onPause() {
- super.onPause();
- refreshDataHandler.removeCallbacks(refreshDataTask);
- }
-
- @Override
- public void onResume() {
- super.onResume();
- if(!this.settings.getBoolean("split_trajectory", false)) {
- refreshDataHandler.postDelayed(refreshDataTask, 500);
- }
- }
-}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/ReplayFragment.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/ReplayFragment.java
deleted file mode 100644
index d15a4a83..00000000
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/ReplayFragment.java
+++ /dev/null
@@ -1,365 +0,0 @@
-package com.openpositioning.PositionMe.presentation.fragment;
-
-import android.os.Bundle;
-import android.os.Handler;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.SeekBar;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.Fragment;
-
-import com.google.android.gms.maps.model.LatLng;
-import com.openpositioning.PositionMe.R;
-import com.openpositioning.PositionMe.presentation.activity.ReplayActivity;
-import com.openpositioning.PositionMe.data.local.TrajParser;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Sub fragment of Replay Activity. Fragment that replays trajectory data on a map.
- *
- * The ReplayFragment is responsible for visualizing and replaying trajectory data captured during
- * previous recordings. It loads trajectory data from a JSON file, updates the map with user movement,
- * and provides UI controls for playback, pause, and seek functionalities.
- *
- * Features:
- * - Loads trajectory data from a file and displays it on a map.
- * - Provides playback controls including play, pause, restart, and go to end.
- * - Updates the trajectory dynamically as playback progresses.
- * - Allows users to manually seek through the recorded trajectory.
- * - Integrates with {@link TrajectoryMapFragment} for map visualization.
- *
- * @see TrajectoryMapFragment The map fragment displaying the trajectory.
- * @see ReplayActivity The activity managing the replay workflow.
- * @see TrajParser Utility class for parsing trajectory data.
- *
- * @author Shu Gu
- */
-public class ReplayFragment extends Fragment {
-
- private static final String TAG = "ReplayFragment";
-
- // GPS start location (received from ReplayActivity)
- private float initialLat = 0f;
- private float initialLon = 0f;
- private String filePath = "";
- private int lastIndex = -1;
-
- // UI Controls
- private TrajectoryMapFragment trajectoryMapFragment;
- private Button playPauseButton, restartButton, exitButton, goEndButton;
- private SeekBar playbackSeekBar;
-
- // Playback-related
- private final Handler playbackHandler = new Handler();
- private final long PLAYBACK_INTERVAL_MS = 500; // milliseconds
- private List replayData = new ArrayList<>();
- private int currentIndex = 0;
- private boolean isPlaying = false;
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- // Retrieve transferred data from ReplayActivity
- if (getArguments() != null) {
- filePath = getArguments().getString(ReplayActivity.EXTRA_TRAJECTORY_FILE_PATH, "");
- initialLat = getArguments().getFloat(ReplayActivity.EXTRA_INITIAL_LAT, 0f);
- initialLon = getArguments().getFloat(ReplayActivity.EXTRA_INITIAL_LON, 0f);
- }
-
- // Log the received data
- Log.i(TAG, "ReplayFragment received data:");
- Log.i(TAG, "Trajectory file path: " + filePath);
- Log.i(TAG, "Initial latitude: " + initialLat);
- Log.i(TAG, "Initial longitude: " + initialLon);
-
- // Check if file exists before parsing
- File trajectoryFile = new File(filePath);
- if (!trajectoryFile.exists()) {
- Log.e(TAG, "ERROR: Trajectory file does NOT exist at: " + filePath);
- return;
- }
- if (!trajectoryFile.canRead()) {
- Log.e(TAG, "ERROR: Trajectory file exists but is NOT readable: " + filePath);
- return;
- }
-
- Log.i(TAG, "Trajectory file confirmed to exist and is readable.");
-
- // Parse the JSON file and prepare replayData using TrajParser
- replayData = TrajParser.parseTrajectoryData(filePath, requireContext(), initialLat, initialLon);
-
- // Log the number of parsed points
- if (replayData != null && !replayData.isEmpty()) {
- Log.i(TAG, "Trajectory data loaded successfully. Total points: " + replayData.size());
- } else {
- Log.e(TAG, "Failed to load trajectory data! replayData is empty or null.");
- }
- }
-
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater,
- @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- return inflater.inflate(R.layout.fragment_replay, container, false);
- }
-
- @Override
- public void onViewCreated(@NonNull View view,
- @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
-
- // Initialize map fragment
- trajectoryMapFragment = (TrajectoryMapFragment)
- getChildFragmentManager().findFragmentById(R.id.replayMapFragmentContainer);
- if (trajectoryMapFragment == null) {
- trajectoryMapFragment = new TrajectoryMapFragment();
- getChildFragmentManager()
- .beginTransaction()
- .replace(R.id.replayMapFragmentContainer, trajectoryMapFragment)
- .commit();
- }
-
-
-
- // 1) Check if the file contains any GNSS data
- boolean gnssExists = hasAnyGnssData(replayData);
-
- if (gnssExists) {
- showGnssChoiceDialog();
- } else {
- // No GNSS data -> automatically use param lat/lon
- if (initialLat != 0f || initialLon != 0f) {
- LatLng startPoint = new LatLng(initialLat, initialLon);
- Log.i(TAG, "Setting initial map position: " + startPoint.toString());
- trajectoryMapFragment.setInitialCameraPosition(startPoint);
- }
- }
-
- // Initialize UI controls
- playPauseButton = view.findViewById(R.id.playPauseButton);
- restartButton = view.findViewById(R.id.restartButton);
- exitButton = view.findViewById(R.id.exitButton);
- goEndButton = view.findViewById(R.id.goEndButton);
- playbackSeekBar = view.findViewById(R.id.playbackSeekBar);
-
- // Set SeekBar max value based on replay data
- if (!replayData.isEmpty()) {
- playbackSeekBar.setMax(replayData.size() - 1);
- }
-
- // Button Listeners
- playPauseButton.setOnClickListener(v -> {
- if (replayData.isEmpty()) {
- Log.w(TAG, "Play/Pause button pressed but replayData is empty.");
- return;
- }
- if (isPlaying) {
- isPlaying = false;
- playPauseButton.setText("Play");
- Log.i(TAG, "Playback paused at index: " + currentIndex);
- } else {
- isPlaying = true;
- playPauseButton.setText("Pause");
- Log.i(TAG, "Playback started from index: " + currentIndex);
- if (currentIndex >= replayData.size()) {
- currentIndex = 0;
- }
- playbackHandler.post(playbackRunnable);
- }
- });
-
- // Restart button listener
- restartButton.setOnClickListener(v -> {
- if (replayData.isEmpty()) return;
- currentIndex = 0;
- playbackSeekBar.setProgress(0);
- Log.i(TAG, "Restart button pressed. Resetting playback to index 0.");
- updateMapForIndex(0);
- });
-
- // Go to End button listener
- goEndButton.setOnClickListener(v -> {
- if (replayData.isEmpty()) return;
- currentIndex = replayData.size() - 1;
- playbackSeekBar.setProgress(currentIndex);
- Log.i(TAG, "Go to End button pressed. Moving to last index: " + currentIndex);
- updateMapForIndex(currentIndex);
- isPlaying = false;
- playPauseButton.setText("Play");
- });
-
- // Exit button listener
- exitButton.setOnClickListener(v -> {
- Log.i(TAG, "Exit button pressed. Exiting replay.");
- if (getActivity() instanceof ReplayActivity) {
- ((ReplayActivity) getActivity()).finishFlow();
- } else {
- requireActivity().onBackPressed();
- }
- });
-
- // SeekBar listener
- playbackSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
- @Override
- public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
- if (fromUser) {
- Log.i(TAG, "SeekBar moved by user. New index: " + progress);
- currentIndex = progress;
- updateMapForIndex(currentIndex);
- }
- }
- @Override public void onStartTrackingTouch(SeekBar seekBar) {}
- @Override public void onStopTrackingTouch(SeekBar seekBar) {}
- });
-
- if (!replayData.isEmpty()) {
- updateMapForIndex(0);
- }
- }
-
-
-
- /**
- * Checks if any ReplayPoint contains a non-null gnssLocation.
- */
- private boolean hasAnyGnssData(List data) {
- for (TrajParser.ReplayPoint point : data) {
- if (point.gnssLocation != null) {
- return true;
- }
- }
- return false;
- }
-
-
- /**
- * Show a simple dialog asking user to pick:
- * 1) GNSS from file
- * 2) Lat/Lon from ReplayActivity arguments
- */
- private void showGnssChoiceDialog() {
- new AlertDialog.Builder(requireContext())
- .setTitle("Choose Starting Location")
- .setMessage("GNSS data is found in the file. Would you like to use the file's GNSS as the start, or the one you manually picked?")
- .setPositiveButton("Use File's GNSS", (dialog, which) -> {
- LatLng firstGnss = getFirstGnssLocation(replayData);
- if (firstGnss != null) {
- setupInitialMapPosition((float) firstGnss.latitude, (float) firstGnss.longitude);
- } else {
- // Fallback if no valid GNSS found
- setupInitialMapPosition(initialLat, initialLon);
- }
- dialog.dismiss();
- })
- .setNegativeButton("Use Manual Set", (dialog, which) -> {
- setupInitialMapPosition(initialLat, initialLon);
- dialog.dismiss();
- })
- .setCancelable(false)
- .show();
- }
-
- private void setupInitialMapPosition(float latitude, float longitude) {
- LatLng startPoint = new LatLng(initialLat, initialLon);
- Log.i(TAG, "Setting initial map position: " + startPoint.toString());
- trajectoryMapFragment.setInitialCameraPosition(startPoint);
- }
-
- /**
- * Retrieve the first available GNSS location from the replay data.
- */
- private LatLng getFirstGnssLocation(List data) {
- for (TrajParser.ReplayPoint point : data) {
- if (point.gnssLocation != null) {
- return new LatLng(replayData.get(0).gnssLocation.latitude, replayData.get(0).gnssLocation.longitude);
- }
- }
- return null; // None found
- }
-
-
- /**
- * Runnable for playback of trajectory data.
- * This runnable is called repeatedly to update the map with the next point in the replayData list.
- */
- private final Runnable playbackRunnable = new Runnable() {
- @Override
- public void run() {
- if (!isPlaying || replayData.isEmpty()) return;
-
- Log.i(TAG, "Playing index: " + currentIndex);
- updateMapForIndex(currentIndex);
- currentIndex++;
- playbackSeekBar.setProgress(currentIndex);
-
- if (currentIndex < replayData.size()) {
- playbackHandler.postDelayed(this, PLAYBACK_INTERVAL_MS);
- } else {
- Log.i(TAG, "Playback completed. Reached end of data.");
- isPlaying = false;
- playPauseButton.setText("Play");
- }
- }
- };
-
-
- /**
- * Update the map with the user location and GNSS location (if available) for the given index.
- * Clears the map and redraws up to the given index.
- *
- * @param newIndex
- */
- private void updateMapForIndex(int newIndex) {
- if (newIndex < 0 || newIndex >= replayData.size()) return;
-
- // Detect if user is playing sequentially (lastIndex + 1)
- // or is skipping around (backwards, or jump forward)
- boolean isSequentialForward = (newIndex == lastIndex + 1);
-
- if (!isSequentialForward) {
- // Clear everything and redraw up to newIndex
- trajectoryMapFragment.clearMapAndReset();
- for (int i = 0; i <= newIndex; i++) {
- TrajParser.ReplayPoint p = replayData.get(i);
- trajectoryMapFragment.updateUserLocation(p.pdrLocation, p.orientation);
- if (p.gnssLocation != null) {
- trajectoryMapFragment.updateGNSS(p.gnssLocation);
- }
- }
- } else {
- // Normal sequential forward step: add just the new point
- TrajParser.ReplayPoint p = replayData.get(newIndex);
- trajectoryMapFragment.updateUserLocation(p.pdrLocation, p.orientation);
- if (p.gnssLocation != null) {
- trajectoryMapFragment.updateGNSS(p.gnssLocation);
- }
- }
-
- lastIndex = newIndex;
- }
-
- @Override
- public void onPause() {
- super.onPause();
- isPlaying = false;
- playbackHandler.removeCallbacks(playbackRunnable);
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- playbackHandler.removeCallbacks(playbackRunnable);
- }
-}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/TrajectoryMapFragment.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/TrajectoryMapFragment.java
deleted file mode 100644
index eb0bad65..00000000
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/TrajectoryMapFragment.java
+++ /dev/null
@@ -1,541 +0,0 @@
-package com.openpositioning.PositionMe.presentation.fragment;
-
-import android.graphics.Color;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.ArrayAdapter;
-import android.widget.Button;
-import android.widget.Spinner;
-import com.google.android.material.switchmaterial.SwitchMaterial;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-
-import com.google.android.gms.maps.OnMapReadyCallback;
-import com.openpositioning.PositionMe.R;
-import com.openpositioning.PositionMe.sensors.SensorFusion;
-import com.openpositioning.PositionMe.utils.IndoorMapManager;
-import com.openpositioning.PositionMe.utils.UtilFunctions;
-import com.google.android.gms.maps.CameraUpdateFactory;
-import com.google.android.gms.maps.GoogleMap;
-import com.google.android.gms.maps.SupportMapFragment;
-import com.google.android.gms.maps.model.*;
-
-import java.util.ArrayList;
-import java.util.List;
-
-
-/**
- * A fragment responsible for displaying a trajectory map using Google Maps.
- *
- * The TrajectoryMapFragment provides a map interface for visualizing movement trajectories,
- * GNSS tracking, and indoor mapping. It manages map settings, user interactions, and real-time
- * updates to user location and GNSS markers.
- *
- * Key Features:
- * - Displays a Google Map with support for different map types (Hybrid, Normal, Satellite).
- * - Tracks and visualizes user movement using polylines.
- * - Supports GNSS position updates and visual representation.
- * - Includes indoor mapping with floor selection and auto-floor adjustments.
- * - Allows user interaction through map controls and UI elements.
- *
- * @see com.openpositioning.PositionMe.presentation.activity.RecordingActivity The activity hosting this fragment.
- * @see com.openpositioning.PositionMe.utils.IndoorMapManager Utility for managing indoor map overlays.
- * @see com.openpositioning.PositionMe.utils.UtilFunctions Utility functions for UI and graphics handling.
- *
- * @author Mate Stodulka
- */
-
-public class TrajectoryMapFragment extends Fragment {
-
- private GoogleMap gMap; // Google Maps instance
- private LatLng currentLocation; // Stores the user's current location
- private Marker orientationMarker; // Marker representing user's heading
- private Marker gnssMarker; // GNSS position marker
- private Polyline polyline; // Polyline representing user's movement path
- private boolean isRed = true; // Tracks whether the polyline color is red
- private boolean isGnssOn = false; // Tracks if GNSS tracking is enabled
-
- private Polyline gnssPolyline; // Polyline for GNSS path
- private LatLng lastGnssLocation = null; // Stores the last GNSS location
-
- private LatLng pendingCameraPosition = null; // Stores pending camera movement
- private boolean hasPendingCameraMove = false; // Tracks if camera needs to move
-
- private IndoorMapManager indoorMapManager; // Manages indoor mapping
- private SensorFusion sensorFusion;
-
-
- // UI
- private Spinner switchMapSpinner;
-
- private SwitchMaterial gnssSwitch;
- private SwitchMaterial autoFloorSwitch;
-
- private com.google.android.material.floatingactionbutton.FloatingActionButton floorUpButton, floorDownButton;
- private Button switchColorButton;
- private Polygon buildingPolygon;
-
-
- public TrajectoryMapFragment() {
- // Required empty public constructor
- }
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater,
- @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- // Inflate the separate layout containing map + map-related UI
- return inflater.inflate(R.layout.fragment_trajectory_map, container, false);
- }
-
- @Override
- public void onViewCreated(@NonNull View view,
- @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
-
- // Grab references to UI controls
- switchMapSpinner = view.findViewById(R.id.mapSwitchSpinner);
- gnssSwitch = view.findViewById(R.id.gnssSwitch);
- autoFloorSwitch = view.findViewById(R.id.autoFloor);
- floorUpButton = view.findViewById(R.id.floorUpButton);
- floorDownButton = view.findViewById(R.id.floorDownButton);
- switchColorButton = view.findViewById(R.id.lineColorButton);
-
- // Setup floor up/down UI hidden initially until we know there's an indoor map
- setFloorControlsVisibility(View.GONE);
-
- // Initialize the map asynchronously
- SupportMapFragment mapFragment = (SupportMapFragment)
- getChildFragmentManager().findFragmentById(R.id.trajectoryMap);
- if (mapFragment != null) {
- mapFragment.getMapAsync(new OnMapReadyCallback() {
- @Override
- public void onMapReady(@NonNull GoogleMap googleMap) {
- // Assign the provided googleMap to your field variable
- gMap = googleMap;
- // Initialize map settings with the now non-null gMap
- initMapSettings(gMap);
-
- // If we had a pending camera move, apply it now
- if (hasPendingCameraMove && pendingCameraPosition != null) {
- gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(pendingCameraPosition, 19f));
- hasPendingCameraMove = false;
- pendingCameraPosition = null;
- }
-
- drawBuildingPolygon();
-
- Log.d("TrajectoryMapFragment", "onMapReady: Map is ready!");
-
-
- }
- });
- }
-
- // Map type spinner setup
- initMapTypeSpinner();
-
- // GNSS Switch
- gnssSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
- isGnssOn = isChecked;
- if (!isChecked && gnssMarker != null) {
- gnssMarker.remove();
- gnssMarker = null;
- }
- });
-
- // Color switch
- switchColorButton.setOnClickListener(v -> {
- if (polyline != null) {
- if (isRed) {
- switchColorButton.setBackgroundColor(Color.BLACK);
- polyline.setColor(Color.BLACK);
- isRed = false;
- } else {
- switchColorButton.setBackgroundColor(Color.RED);
- polyline.setColor(Color.RED);
- isRed = true;
- }
- }
- });
-
- // Floor up/down logic
- autoFloorSwitch.setOnCheckedChangeListener((compoundButton, isChecked) -> {
-
- //TODO - fix the sensor fusion method to get the elevation (cannot get it from the current method)
-// float elevationVal = sensorFusion.getElevation();
-// indoorMapManager.setCurrentFloor((int)(elevationVal/indoorMapManager.getFloorHeight())
-// ,true);
- });
-
- floorUpButton.setOnClickListener(v -> {
- // If user manually changes floor, turn off auto floor
- autoFloorSwitch.setChecked(false);
- if (indoorMapManager != null) {
- indoorMapManager.increaseFloor();
- }
- });
-
- floorDownButton.setOnClickListener(v -> {
- autoFloorSwitch.setChecked(false);
- if (indoorMapManager != null) {
- indoorMapManager.decreaseFloor();
- }
- });
- }
-
- /**
- * Initialize the map settings with the provided GoogleMap instance.
- *
- * The method sets basic map settings, initializes the indoor map manager,
- * and creates an empty polyline for user movement tracking.
- * The method also initializes the GNSS polyline for tracking GNSS path.
- * The method sets the map type to Hybrid and initializes the map with these settings.
- *
- * @param map
- */
-
- private void initMapSettings(GoogleMap map) {
- // Basic map settings
- map.getUiSettings().setCompassEnabled(true);
- map.getUiSettings().setTiltGesturesEnabled(true);
- map.getUiSettings().setRotateGesturesEnabled(true);
- map.getUiSettings().setScrollGesturesEnabled(true);
- map.setMapType(GoogleMap.MAP_TYPE_HYBRID);
-
- // Initialize indoor manager
- indoorMapManager = new IndoorMapManager(map);
-
- // Initialize an empty polyline
- polyline = map.addPolyline(new PolylineOptions()
- .color(Color.RED)
- .width(5f)
- .add() // start empty
- );
-
- // GNSS path in blue
- gnssPolyline = map.addPolyline(new PolylineOptions()
- .color(Color.BLUE)
- .width(5f)
- .add() // start empty
- );
- }
-
-
- /**
- * Initialize the map type spinner with the available map types.
- *
- * The spinner allows the user to switch between different map types
- * (e.g. Hybrid, Normal, Satellite) to customize their map view.
- * The spinner is populated with the available map types and listens
- * for user selection to update the map accordingly.
- * The map type is updated directly on the GoogleMap instance.
- *
- * Note: The spinner is initialized with the default map type (Hybrid).
- * The map type is updated on user selection.
- *
- *
- * @see com.google.android.gms.maps.GoogleMap The GoogleMap instance to update map type.
- */
- private void initMapTypeSpinner() {
- if (switchMapSpinner == null) return;
- String[] maps = new String[]{
- getString(R.string.hybrid),
- getString(R.string.normal),
- getString(R.string.satellite)
- };
- ArrayAdapter adapter = new ArrayAdapter<>(
- requireContext(),
- android.R.layout.simple_spinner_dropdown_item,
- maps
- );
- switchMapSpinner.setAdapter(adapter);
-
- switchMapSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
- @Override
- public void onItemSelected(AdapterView> parent, View view,
- int position, long id) {
- if (gMap == null) return;
- switch (position){
- case 0:
- gMap.setMapType(GoogleMap.MAP_TYPE_HYBRID);
- break;
- case 1:
- gMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
- break;
- case 2:
- gMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE);
- break;
- }
- }
- @Override
- public void onNothingSelected(AdapterView> parent) {}
- });
- }
-
- /**
- * Update the user's current location on the map, create or move orientation marker,
- * and append to polyline if the user actually moved.
- *
- * @param newLocation The new location to plot.
- * @param orientation The user’s heading (e.g. from sensor fusion).
- */
- public void updateUserLocation(@NonNull LatLng newLocation, float orientation) {
- if (gMap == null) return;
-
- // Keep track of current location
- LatLng oldLocation = this.currentLocation;
- this.currentLocation = newLocation;
-
- // If no marker, create it
- if (orientationMarker == null) {
- orientationMarker = gMap.addMarker(new MarkerOptions()
- .position(newLocation)
- .flat(true)
- .title("Current Position")
- .icon(BitmapDescriptorFactory.fromBitmap(
- UtilFunctions.getBitmapFromVector(requireContext(),
- R.drawable.ic_baseline_navigation_24)))
- );
- gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(newLocation, 19f));
- } else {
- // Update marker position + orientation
- orientationMarker.setPosition(newLocation);
- orientationMarker.setRotation(orientation);
- // Move camera a bit
- gMap.moveCamera(CameraUpdateFactory.newLatLng(newLocation));
- }
-
- // Extend polyline if movement occurred
- if (oldLocation != null && !oldLocation.equals(newLocation) && polyline != null) {
- List points = new ArrayList<>(polyline.getPoints());
- points.add(newLocation);
- polyline.setPoints(points);
- }
-
- // Update indoor map overlay
- if (indoorMapManager != null) {
- indoorMapManager.setCurrentLocation(newLocation);
- setFloorControlsVisibility(indoorMapManager.getIsIndoorMapSet() ? View.VISIBLE : View.GONE);
- }
- }
-
-
-
- /**
- * Set the initial camera position for the map.
- *
- * The method sets the initial camera position for the map when it is first loaded.
- * If the map is already ready, the camera is moved immediately.
- * If the map is not ready, the camera position is stored until the map is ready.
- * The method also tracks if there is a pending camera move.
- *
- * @param startLocation The initial camera position to set.
- */
- public void setInitialCameraPosition(@NonNull LatLng startLocation) {
- // If the map is already ready, move camera immediately
- if (gMap != null) {
- gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(startLocation, 19f));
- } else {
- // Otherwise, store it until onMapReady
- pendingCameraPosition = startLocation;
- hasPendingCameraMove = true;
- }
- }
-
-
- /**
- * Get the current user location on the map.
- * @return The current user location as a LatLng object.
- */
- public LatLng getCurrentLocation() {
- return currentLocation;
- }
-
- /**
- * Called when we want to set or update the GNSS marker position
- */
- public void updateGNSS(@NonNull LatLng gnssLocation) {
- if (gMap == null) return;
- if (!isGnssOn) return;
-
- if (gnssMarker == null) {
- // Create the GNSS marker for the first time
- gnssMarker = gMap.addMarker(new MarkerOptions()
- .position(gnssLocation)
- .title("GNSS Position")
- .icon(BitmapDescriptorFactory
- .defaultMarker(BitmapDescriptorFactory.HUE_AZURE)));
- lastGnssLocation = gnssLocation;
- } else {
- // Move existing GNSS marker
- gnssMarker.setPosition(gnssLocation);
-
- // Add a segment to the blue GNSS line, if this is a new location
- if (lastGnssLocation != null && !lastGnssLocation.equals(gnssLocation)) {
- List gnssPoints = new ArrayList<>(gnssPolyline.getPoints());
- gnssPoints.add(gnssLocation);
- gnssPolyline.setPoints(gnssPoints);
- }
- lastGnssLocation = gnssLocation;
- }
- }
-
-
- /**
- * Remove GNSS marker if user toggles it off
- */
- public void clearGNSS() {
- if (gnssMarker != null) {
- gnssMarker.remove();
- gnssMarker = null;
- }
- }
-
- /**
- * Whether user is currently showing GNSS or not
- */
- public boolean isGnssEnabled() {
- return isGnssOn;
- }
-
- private void setFloorControlsVisibility(int visibility) {
- floorUpButton.setVisibility(visibility);
- floorDownButton.setVisibility(visibility);
- autoFloorSwitch.setVisibility(visibility);
- }
-
- public void clearMapAndReset() {
- if (polyline != null) {
- polyline.remove();
- polyline = null;
- }
- if (gnssPolyline != null) {
- gnssPolyline.remove();
- gnssPolyline = null;
- }
- if (orientationMarker != null) {
- orientationMarker.remove();
- orientationMarker = null;
- }
- if (gnssMarker != null) {
- gnssMarker.remove();
- gnssMarker = null;
- }
- lastGnssLocation = null;
- currentLocation = null;
-
- // Re-create empty polylines with your chosen colors
- if (gMap != null) {
- polyline = gMap.addPolyline(new PolylineOptions()
- .color(Color.RED)
- .width(5f)
- .add());
- gnssPolyline = gMap.addPolyline(new PolylineOptions()
- .color(Color.BLUE)
- .width(5f)
- .add());
- }
- }
-
- /**
- * Draw the building polygon on the map
- *
- * The method draws a polygon representing the building on the map.
- * The polygon is drawn with specific vertices and colors to represent
- * different buildings or areas on the map.
- * The method removes the old polygon if it exists and adds the new polygon
- * to the map with the specified options.
- * The method logs the number of vertices in the polygon for debugging.
- *
- *
- * Note: The method uses hard-coded vertices for the building polygon.
- *
- *
- *
- * See: {@link com.google.android.gms.maps.model.PolygonOptions} The options for the new polygon.
- */
- private void drawBuildingPolygon() {
- if (gMap == null) {
- Log.e("TrajectoryMapFragment", "GoogleMap is not ready");
- return;
- }
-
- // nuclear building polygon vertices
- LatLng nucleus1 = new LatLng(55.92279538827796, -3.174612147506538);
- LatLng nucleus2 = new LatLng(55.92278121423647, -3.174107900816096);
- LatLng nucleus3 = new LatLng(55.92288405733954, -3.173843694667146);
- LatLng nucleus4 = new LatLng(55.92331786793876, -3.173832892645086);
- LatLng nucleus5 = new LatLng(55.923337194112555, -3.1746284301397387);
-
-
- // nkml building polygon vertices
- LatLng nkml1 = new LatLng(55.9230343434213, -3.1751847990731954);
- LatLng nkml2 = new LatLng(55.923032840563366, -3.174777103346131);
- LatLng nkml4 = new LatLng(55.92280139974615, -3.175195527934348);
- LatLng nkml3 = new LatLng(55.922793885410734, -3.1747958788136867);
-
- LatLng fjb1 = new LatLng(55.92269205199916, -3.1729563477188774);//left top
- LatLng fjb2 = new LatLng(55.922822801570994, -3.172594249522305);
- LatLng fjb3 = new LatLng(55.92223512226413, -3.171921917547244);
- LatLng fjb4 = new LatLng(55.9221071265519, -3.1722813131202097);
-
- LatLng faraday1 = new LatLng(55.92242866264128, -3.1719553662011815);
- LatLng faraday2 = new LatLng(55.9224966752294, -3.1717846714743474);
- LatLng faraday3 = new LatLng(55.922271383074154, -3.1715191463437162);
- LatLng faraday4 = new LatLng(55.92220124468304, -3.171705013935158);
-
-
-
- PolygonOptions buildingPolygonOptions = new PolygonOptions()
- .add(nucleus1, nucleus2, nucleus3, nucleus4, nucleus5)
- .strokeColor(Color.RED) // Red border
- .strokeWidth(10f) // Border width
- //.fillColor(Color.argb(50, 255, 0, 0)) // Semi-transparent red fill
- .zIndex(1); // Set a higher zIndex to ensure it appears above other overlays
-
- // Options for the new polygon
- PolygonOptions buildingPolygonOptions2 = new PolygonOptions()
- .add(nkml1, nkml2, nkml3, nkml4, nkml1)
- .strokeColor(Color.BLUE) // Blue border
- .strokeWidth(10f) // Border width
- // .fillColor(Color.argb(50, 0, 0, 255)) // Semi-transparent blue fill
- .zIndex(1); // Set a higher zIndex to ensure it appears above other overlays
-
- PolygonOptions buildingPolygonOptions3 = new PolygonOptions()
- .add(fjb1, fjb2, fjb3, fjb4, fjb1)
- .strokeColor(Color.GREEN) // Green border
- .strokeWidth(10f) // Border width
- //.fillColor(Color.argb(50, 0, 255, 0)) // Semi-transparent green fill
- .zIndex(1); // Set a higher zIndex to ensure it appears above other overlays
-
- PolygonOptions buildingPolygonOptions4 = new PolygonOptions()
- .add(faraday1, faraday2, faraday3, faraday4, faraday1)
- .strokeColor(Color.YELLOW) // Yellow border
- .strokeWidth(10f) // Border width
- //.fillColor(Color.argb(50, 255, 255, 0)) // Semi-transparent yellow fill
- .zIndex(1); // Set a higher zIndex to ensure it appears above other overlays
-
-
- // Remove the old polygon if it exists
- if (buildingPolygon != null) {
- buildingPolygon.remove();
- }
-
- // Add the polygon to the map
- buildingPolygon = gMap.addPolygon(buildingPolygonOptions);
- gMap.addPolygon(buildingPolygonOptions2);
- gMap.addPolygon(buildingPolygonOptions3);
- gMap.addPolygon(buildingPolygonOptions4);
- Log.d("TrajectoryMapFragment", "Building polygon added, vertex count: " + buildingPolygon.getPoints().size());
- }
-
-
-}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/SensorInfoListAdapter.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/SensorInfoListAdapter.java
deleted file mode 100644
index 4315328d..00000000
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/SensorInfoListAdapter.java
+++ /dev/null
@@ -1,85 +0,0 @@
-package com.openpositioning.PositionMe.presentation.viewitems;
-
-import android.content.Context;
-import android.view.LayoutInflater;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.openpositioning.PositionMe.R;
-import com.openpositioning.PositionMe.sensors.SensorInfo;
-
-import java.util.List;
-import java.util.Objects;
-
-/**
- * Adapter used for displaying sensor info data.
- *
- * @see SensorInfoViewHolder corresponding View Holder class
- * @see com.openpositioning.PositionMe.R.layout#item_sensorinfo_card_view xml layout file
- *
- * @author Mate Stodulka
- */
-public class SensorInfoListAdapter extends RecyclerView.Adapter {
-
- Context context;
- List sensorInfoList;
-
- /**
- * Default public constructor with context for inflating views and list to be displayed.
- *
- * @param context application context to enable inflating views used in the list.
- * @param sensorInfoList list of SensorInfo objects to be displayed in the list.
- *
- * @see SensorInfo the data class.
- */
- public SensorInfoListAdapter(Context context, List sensorInfoList) {
- this.context = context;
- this.sensorInfoList = sensorInfoList;
- }
-
- /**
- * {@inheritDoc}
- * @see com.openpositioning.PositionMe.R.layout#item_sensorinfo_card_view xml layout file
- */
- @NonNull
- @Override
- public SensorInfoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- return new SensorInfoViewHolder(LayoutInflater.from(context).inflate(R.layout.item_sensorinfo_card_view, parent, false));
- }
-
- /**
- * {@inheritDoc}
- * Formats and assigns the data fields from the SensorInfo object to the TextView fields.
- *
- * @see SensorInfo data class
- * @see com.openpositioning.PositionMe.R.string formatting for strings.
- * @see com.openpositioning.PositionMe.R.layout#item_sensorinfo_card_view xml layout file
- */
- @Override
- public void onBindViewHolder(@NonNull SensorInfoViewHolder holder, int position) {
- holder.name.setText(sensorInfoList.get(position).getName());
-
- String vendorString = context.getString(R.string.vendor, sensorInfoList.get(position).getVendor());
- holder.vendor.setText(vendorString);
-
- String resolutionString = context.getString(R.string.resolution, String.format("%.03g", sensorInfoList.get(position).getResolution()));
- holder.resolution.setText(resolutionString);
- String powerString = context.getString(R.string.power, Objects.toString(sensorInfoList.get(position).getPower(), "N/A"));
- holder.power.setText(powerString);
- String versionString = context.getString(R.string.version, Objects.toString(sensorInfoList.get(position).getVersion(), "N/A"));
- holder.version.setText(versionString);
- }
-
- /**
- * {@inheritDoc}
- * Number of SensorInfo objects.
- *
- * @see SensorInfo
- */
- @Override
- public int getItemCount() {
- return sensorInfoList.size();
- }
-}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/TrajDownloadListAdapter.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/TrajDownloadListAdapter.java
deleted file mode 100644
index 7de29c8a..00000000
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/TrajDownloadListAdapter.java
+++ /dev/null
@@ -1,295 +0,0 @@
-package com.openpositioning.PositionMe.presentation.viewitems;
-
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.Iterator;
-import java.io.File;
-import java.io.FileReader;
-import java.io.BufferedReader;
-
-import android.content.Intent;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Environment;
-import android.os.FileObserver;
-import android.content.Context;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.core.content.ContextCompat;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.google.android.material.button.MaterialButton;
-import com.openpositioning.PositionMe.R;
-import com.openpositioning.PositionMe.data.remote.ServerCommunications;
-import com.openpositioning.PositionMe.presentation.activity.ReplayActivity;
-import com.openpositioning.PositionMe.presentation.fragment.FilesFragment;
-
-import org.json.JSONObject;
-
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Adapter used for displaying trajectory metadata in a RecyclerView list.
- * This adapter binds trajectory metadata from the server to individual view items.
- * The download status is indicated via a button with different icons.
- * The adapter also listens for file changes using FileObserver to update the download records in real time.
- * A local set of "downloading" trajectory IDs is maintained to support simultaneous downloads.
- * @see TrajDownloadViewHolder for the corresponding view holder.
- * @see FilesFragment for details on how the data is generated.
- * @see ServerCommunications for where the response items are received.
- */
-public class TrajDownloadListAdapter extends RecyclerView.Adapter {
-
- // Date-time formatter used to format date and time.
- private static final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
-
- private final Context context;
- private final List> responseItems;
- private final DownloadClickListener listener;
-
- // FileObserver to monitor modifications to the "download_records.json" file.
- private FileObserver fileObserver;
-
- // Set to keep track of trajectory IDs that are currently downloading.
- private final Set downloadingTrajIds = new HashSet<>();
-
- /**
- * Constructor for the adapter.
- *
- * @param context Application context used for inflating layouts.
- * @param responseItems List of response items from the server.
- * @param listener Callback listener for handling download click events.
- */
- public TrajDownloadListAdapter(Context context, List> responseItems, DownloadClickListener listener) {
- this.context = context;
- this.responseItems = responseItems;
- this.listener = listener;
- // Load the local download records.
- loadDownloadRecords();
- // Initialize the FileObserver to listen for changes in the download records file.
- initFileObserver();
- }
-
- /**
- * Loads the local download records from storage.
- * The records are stored in a JSON file located in the app-specific Downloads directory.
- * After loading, any trajectory IDs that have now finished downloading are removed
- * from the downloading set.
- */
- private void loadDownloadRecords() {
- try {
- File file = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "download_records.json");
- if (file.exists()) {
- // Read the file line by line to reduce memory usage.
- StringBuilder jsonBuilder = new StringBuilder();
- try (BufferedReader reader = new BufferedReader(new FileReader(file), 8192)) { // Increase buffer size
- String line;
- while ((line = reader.readLine()) != null) {
- jsonBuilder.append(line);
- }
- }
-
- // Parse the JSON content.
- JSONObject jsonObject = new JSONObject(jsonBuilder.toString());
- ServerCommunications.downloadRecords.clear();
-
- // Preallocate HashMap capacity to reduce resizing overhead.
- int estimatedSize = jsonObject.length();
- ServerCommunications.downloadRecords = new HashMap<>(estimatedSize * 2);
-
- // Iterate through keys in the JSON object.
- for (Iterator keys = jsonObject.keys(); keys.hasNext(); ) {
- String key = keys.next();
- JSONObject recordDetails = jsonObject.getJSONObject(key);
- // Use the record's "id" if available, otherwise use the key.
- String id = recordDetails.optString("id", key);
- ServerCommunications.downloadRecords.put(id, recordDetails);
- }
-
- System.out.println("Download records loaded: " + ServerCommunications.downloadRecords);
-
- // Remove any IDs from the downloading set that are now present in the download records.
- // This ensures the "downloading" state is removed when the download completes.
- downloadingTrajIds.removeIf(id -> ServerCommunications.downloadRecords.containsKey(id));
-
- // Refresh the RecyclerView UI on the main thread.
- new Handler(Looper.getMainLooper()).post(() -> {
- notifyDataSetChanged();
- System.out.println("RecyclerView fully refreshed after loading records.");
- });
- } else {
- System.out.println("Download records file not found.");
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- /**
- * Initializes the FileObserver to listen for modifications on the "download_records.json" file.
- * When the file is modified, it reloads the download records and refreshes the UI.
- */
- private void initFileObserver() {
- File downloadsFolder = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
- if (downloadsFolder == null) {
- return;
- }
- // Create a FileObserver for the directory where the file is located.
- fileObserver = new FileObserver(downloadsFolder.getAbsolutePath(), FileObserver.MODIFY) {
- @Override
- public void onEvent(int event, String path) {
- // Only act if the modified file is "download_records.json".
- if (path != null && path.equals("download_records.json")) {
- Log.i("FileObserver", "download_records.json has been modified.");
- // On file modification, load the records and update the UI on the main thread.
- new Handler(Looper.getMainLooper()).post(() -> {
- loadDownloadRecords();
- });
- }
- }
- };
- fileObserver.startWatching();
- }
-
- /**
- * Creates a new view holder for a trajectory item.
- *
- * @param parent The parent view group.
- * @param viewType The view type.
- * @return A new instance of TrajDownloadViewHolder.
- */
- @NonNull
- @Override
- public TrajDownloadViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- return new TrajDownloadViewHolder(LayoutInflater.from(context)
- .inflate(R.layout.item_trajectorycard_view, parent, false), listener);
- }
-
- /**
- * Binds data to the view holder.
- * Formats and assigns trajectory metadata fields to the corresponding views.
- * The button state is determined as follows:
- * - If the trajectory is present in the download records, it is set as "downloaded".
- * - Else if the trajectory is in the downloading set, it is set as "downloading".
- * - Otherwise, it is set as "not downloaded".
- * @param holder The view holder to bind data to.
- * @param position The position of the item in the list.
- */
- @Override
- public void onBindViewHolder(@NonNull TrajDownloadViewHolder holder, int position) {
- // Retrieve the trajectory id from the response item.
- String id = responseItems.get(position).get("id");
- holder.getTrajId().setText(id);
-
- // Adjust text size based on the id length.
- if (id != null && id.length() > 2) {
- holder.getTrajId().setTextSize(58);
- } else {
- holder.getTrajId().setTextSize(65);
- }
-
- // Parse and format the submission date.
- String dateSubmittedStr = responseItems.get(position).get("date_submitted");
- assert dateSubmittedStr != null;
- holder.getTrajDate().setText(
- dateFormat.format(
- LocalDateTime.parse(dateSubmittedStr.split("\\.")[0])
- )
- );
-
- // Determine if the trajectory is already downloaded by checking the records.
- JSONObject recordDetails = ServerCommunications.downloadRecords.get(id);
- boolean matched = recordDetails != null;
- String filePath = null;
-
- if (matched) {
- try {
- String fileName = recordDetails.optString("file_name", null);
- if (fileName != null) {
- File file = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), fileName);
- filePath = file.getAbsolutePath();
- }
- // Set the button state to "downloaded".
- setButtonState(holder.downloadButton, 1);
- } catch (Exception e) {
- e.printStackTrace();
- }
- } else if (downloadingTrajIds.contains(id)) {
- // If the item is still being downloaded, set the button state to "downloading".
- setButtonState(holder.downloadButton, 2);
- } else {
- // Otherwise, the item is not downloaded.
- setButtonState(holder.downloadButton, 0);
- }
-
- // Copy matched status and filePath to final variables for use in the lambda expression.
- final boolean finalMatched = matched;
- final String finalFilePath = filePath;
-
- // Set the click listener for the download button.
- holder.downloadButton.setOnClickListener(v -> {
- String trajId = responseItems.get(position).get("id");
-
- if (finalMatched) {
- // If the item is already downloaded, start ReplayActivity to display the trajectory.
- if (finalFilePath != null) {
- Intent intent = new Intent(context, ReplayActivity.class);
- intent.putExtra(ReplayActivity.EXTRA_TRAJECTORY_FILE_PATH, finalFilePath);
- context.startActivity(intent);
- }
- } else {
- // If the item is not downloaded, trigger the download action.
- listener.onPositionClicked(position);
- // Mark the trajectory as downloading.
- downloadingTrajIds.add(trajId);
- // Immediately update the button state to "downloading".
- setButtonState(holder.downloadButton, 2);
- // The FileObserver will update the UI when the file changes.
- }
- });
-
- holder.downloadButton.invalidate();
- }
-
- /**
- * Returns the number of items in the response list.
- *
- * @return The size of the responseItems list.
- */
- @Override
- public int getItemCount() {
- return responseItems.size();
- }
-
- /**
- * Sets the appearance of the button based on its state.
- *
- * @param button The MaterialButton to update.
- * @param state The state of the button:
- * 0 - Not downloaded,
- * 1 - Downloaded,
- * 2 - Downloading.
- */
- private void setButtonState(MaterialButton button, int state) {
- if (state == 1) {
- button.setIconResource(R.drawable.ic_baseline_play_circle_filled_24);
- button.setIconTintResource(R.color.md_theme_onPrimary);
- button.setBackgroundTintList(ContextCompat.getColorStateList(context, R.color.md_theme_primary));
- } else if (state == 2) {
- button.setIconResource(R.drawable.baseline_data_usage_24);
- button.setIconTintResource(R.color.md_theme_onPrimary);
- button.setBackgroundTintList(ContextCompat.getColorStateList(context, R.color.goldYellow));
- } else {
- button.setIconResource(R.drawable.ic_baseline_download_24);
- button.setIconTintResource(R.color.md_theme_onSecondary);
- button.setBackgroundTintList(ContextCompat.getColorStateList(context, R.color.md_theme_light_primary));
- }
- }
-}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/TrajDownloadViewHolder.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/TrajDownloadViewHolder.java
deleted file mode 100644
index af14249f..00000000
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/TrajDownloadViewHolder.java
+++ /dev/null
@@ -1,75 +0,0 @@
-package com.openpositioning.PositionMe.presentation.viewitems;
-
-import android.view.View;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.google.android.material.button.MaterialButton;
-import com.openpositioning.PositionMe.R;
-import com.openpositioning.PositionMe.presentation.fragment.FilesFragment;
-
-import java.lang.ref.WeakReference;
-
-/**
- * View holder class for the RecyclerView displaying Trajectory download data.
- *
- * @see TrajDownloadListAdapter the corresponding list adapter.
- * @see com.openpositioning.PositionMe.R.layout#item_trajectorycard_view xml layout file
- *
- * @author Mate Stodulka
- */
-public class TrajDownloadViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
-
- private final TextView trajId;
- private final TextView trajDate;
- final MaterialButton downloadButton;
- private final WeakReference listenerReference;
-
- /**
- * {@inheritDoc}
- * Assign TextView fields corresponding to Trajectory metadata.
- *
- * @param listener DownloadClickListener to enable acting on clicks on items.
- * @see FilesFragment generating the data and implementing the listener.
- */
- public TrajDownloadViewHolder(@NonNull View itemView, DownloadClickListener listener) {
- super(itemView);
- this.listenerReference = new WeakReference<>(listener);
- this.trajId = itemView.findViewById(R.id.trajectoryIdItem);
- this.trajDate = itemView.findViewById(R.id.trajectoryDateItem);
- this.downloadButton = itemView.findViewById(R.id.downloadTrajectoryButton);
-
- this.downloadButton.setOnClickListener(this);
- }
-
- /**
- * Public getter for trajId.
- */
- public TextView getTrajId() {
- return trajId;
- }
-
- /**
- * Public getter for trajDate.
- */
- public TextView getTrajDate() {
- return trajDate;
- }
-
- /**
- * Calls the onPositionClick function on the listenerReference object.
- */
- @Override
- public void onClick(View view) {
- listenerReference.get().onPositionClicked(getAdapterPosition());
- DownloadClickListener listener = listenerReference.get();
- if (listener != null) {
- listener.onPositionClicked(getAdapterPosition());
- System.out.println("Click detected at position: " + getAdapterPosition());
- } else {
- System.err.println("Listener reference is null.");
- }
- }
-}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/GNSSDataProcessor.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/GNSSDataProcessor.java
index 579e344c..8fd6378f 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/sensors/GNSSDataProcessor.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/GNSSDataProcessor.java
@@ -6,6 +6,7 @@
import android.content.pm.PackageManager;
import android.location.LocationListener;
import android.location.LocationManager;
+import android.os.Build;
import android.widget.Toast;
import androidx.core.app.ActivityCompat;
@@ -76,17 +77,23 @@ public GNSSDataProcessor(Context context, LocationListener locationListener) {
* @return boolean true if all permissions are granted for location access, false otherwise.
*/
private boolean checkLocationPermissions() {
- int coarseLocationPermission = ActivityCompat.checkSelfPermission(this.context,
- Manifest.permission.ACCESS_COARSE_LOCATION);
- int fineLocationPermission = ActivityCompat.checkSelfPermission(this.context,
- Manifest.permission.ACCESS_FINE_LOCATION);
- int internetPermission = ActivityCompat.checkSelfPermission(this.context,
- Manifest.permission.INTERNET);
+ if (Build.VERSION.SDK_INT >= 23) {
- // Return missing permissions
- return coarseLocationPermission == PackageManager.PERMISSION_GRANTED &&
- fineLocationPermission == PackageManager.PERMISSION_GRANTED &&
- internetPermission == PackageManager.PERMISSION_GRANTED;
+ int coarseLocationPermission = ActivityCompat.checkSelfPermission(this.context,
+ Manifest.permission.ACCESS_COARSE_LOCATION);
+ int fineLocationPermission = ActivityCompat.checkSelfPermission(this.context,
+ Manifest.permission.ACCESS_FINE_LOCATION);
+ int internetPermission = ActivityCompat.checkSelfPermission(this.context,
+ Manifest.permission.INTERNET);
+
+ // Return missing permissions
+ return coarseLocationPermission == PackageManager.PERMISSION_GRANTED &&
+ fineLocationPermission == PackageManager.PERMISSION_GRANTED &&
+ internetPermission == PackageManager.PERMISSION_GRANTED;
+ } else {
+ // Permissions are granted by default
+ return true;
+ }
}
/**
@@ -98,13 +105,22 @@ private boolean checkLocationPermissions() {
*/
@SuppressLint("MissingPermission")
public void startLocationUpdates() {
- //if (sharedPreferences.getBoolean("location", true)) {
boolean permissionGranted = checkLocationPermissions();
if (permissionGranted && locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) &&
- locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)){
+ locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
- locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, locationListener);
- locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, locationListener);
+ locationManager.requestLocationUpdates(
+ LocationManager.GPS_PROVIDER,
+ 500, // 最小时间间隔(毫秒)- 从100ms增加到500ms
+ 1, // 最小距离变化(米)- 从0米增加到1米
+ locationListener
+ );
+ locationManager.requestLocationUpdates(
+ LocationManager.NETWORK_PROVIDER,
+ 500, // 最小时间间隔(毫秒)- 从100ms增加到500ms
+ 1, // 最小距离变化(米)- 从0米增加到1米
+ locationListener
+ );
}
else if(permissionGranted && !locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)){
Toast.makeText(context, "Open GPS", Toast.LENGTH_LONG).show();
diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/Observable.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/Observable.java
index dc7e0c73..7732223d 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/sensors/Observable.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/Observable.java
@@ -14,7 +14,7 @@ public interface Observable {
*
* @param o instance of a class implementing the Observer interface
*/
- public void registerObserver(com.openpositioning.PositionMe.sensors.Observer o);
+ public void registerObserver(Observer o);
/**
* Notify observers of changes to relevant data structures. If there are multiple data structures
diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorFusion.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorFusion.java
index 6eca847c..2dd9c453 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorFusion.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorFusion.java
@@ -10,19 +10,19 @@
import android.location.LocationListener;
import android.os.Build;
import android.os.PowerManager;
-import android.os.SystemClock;
import android.util.Log;
-import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import com.google.android.gms.maps.model.LatLng;
-import com.openpositioning.PositionMe.presentation.activity.MainActivity;
-import com.openpositioning.PositionMe.utils.PathView;
-import com.openpositioning.PositionMe.utils.PdrProcessing;
-import com.openpositioning.PositionMe.data.remote.ServerCommunications;
+import com.openpositioning.PositionMe.BuildingPolygon;
+import com.openpositioning.PositionMe.IndoorMapManager;
+import com.openpositioning.PositionMe.MainActivity;
+import com.openpositioning.PositionMe.PathView;
+import com.openpositioning.PositionMe.PdrProcessing;
+import com.openpositioning.PositionMe.ServerCommunications;
import com.openpositioning.PositionMe.Traj;
-import com.openpositioning.PositionMe.presentation.fragment.SettingsFragment;
+import com.openpositioning.PositionMe.utils.LocationLogger;
import org.json.JSONException;
import org.json.JSONObject;
@@ -59,17 +59,57 @@
*/
public class SensorFusion implements SensorEventListener, Observer {
- // Store the last event timestamps for each sensor type
- private HashMap lastEventTimestamps = new HashMap<>();
- private HashMap eventCounts = new HashMap<>();
+ //region Static variables
+ // Singleton Class
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////
+ private List trajectoryPoints = new ArrayList<>();
+
+ // 开始记录时清空轨迹数据
+
+ // 实时添加轨迹点
+ public void addTrajectoryPoint(float latitude, float longitude) {
+ // 更新滑动窗口
+ latitudeWindow[windowIndex] = latitude;
+ longitudeWindow[windowIndex] = longitude;
+ windowIndex = (windowIndex + 1) % TRAJECTORY_WINDOW_SIZE;
+ if (windowIndex == 0) {
+ windowFull = true;
+ }
- long maxReportLatencyNs = 0; // Disable batching to deliver events immediately
+ // 计算平滑后的位置
+ float smoothedLat = 0;
+ float smoothedLon = 0;
+ int count = windowFull ? TRAJECTORY_WINDOW_SIZE : windowIndex;
+
+ if (count > 0) {
+ // 使用加权移动平均
+ float totalWeight = 0;
+ for (int i = 0; i < count; i++) {
+ int idx = (windowIndex - 1 - i + TRAJECTORY_WINDOW_SIZE) % TRAJECTORY_WINDOW_SIZE;
+ float weight = (count - i) / (float)count; // 较新的数据权重更大
+ smoothedLat += latitudeWindow[idx] * weight;
+ smoothedLon += longitudeWindow[idx] * weight;
+ totalWeight += weight;
+ }
+ smoothedLat /= totalWeight;
+ smoothedLon /= totalWeight;
+
+ // 添加平滑后的轨迹点
+ trajectoryPoints.add(new float[]{smoothedLat, smoothedLon});
+ } else {
+ // 如果没有历史数据,直接添加当前点
+ trajectoryPoints.add(new float[]{latitude, longitude});
+ }
+ }
- // Define a threshold for large time gaps (in milliseconds)
- private static final long LARGE_GAP_THRESHOLD_MS = 500; // Adjust this if needed
+ // 获取记录的轨迹点
+ public List getTrajectoryPoints() {
+ return trajectoryPoints;
+ }
- //region Static variables
- // Singleton Class
+
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////
private static final SensorFusion sensorFusion = new SensorFusion();
// Static constant for calculations with milliseconds
private static final long TIME_CONST = 10;
@@ -84,7 +124,6 @@ public class SensorFusion implements SensorEventListener, Observer {
//region Instance variables
// Keep device awake while recording
private PowerManager.WakeLock wakeLock;
- private Context appContext;
// Settings
private SharedPreferences settings;
@@ -117,7 +156,6 @@ public class SensorFusion implements SensorEventListener, Observer {
// Variables to help with timed events
private long absoluteStartTime;
private long bootTime;
- long lastStepTime = 0;
// Timer object for scheduling data recording
private Timer storeTrajectoryTimer;
// Counters for dividing timer to record data every 1 second/ every 5 seconds
@@ -159,6 +197,55 @@ public class SensorFusion implements SensorEventListener, Observer {
// WiFi positioning object
private WiFiPositioning wiFiPositioning;
+ private LocationLogger locationLogger;
+ private Context context;
+
+ // 修改常量定义
+ private static float STANDARD_PRESSURE = 1015.2f;
+ private static final float DEFAULT_FLOOR_HEIGHT = 2.5f; // 默认楼层高度
+ private static final float ALTITUDE_OFFSET = 0.0f; // 基准高度偏移量
+ private float currentFloorHeight = DEFAULT_FLOOR_HEIGHT; // 当前使用的楼层高度
+ private int currentFloor = 0;
+ private boolean isInSpecialBuilding = false; // 是否在特殊建筑物内
+
+ // 在 SensorFusion 类中添加观察者列表
+ private List floorObservers = new ArrayList<>();
+
+ // 重写EKF相关变量
+ private Timer ekfTimer;
+ private static final int EKF_UPDATE_INTERVAL = 100; // 更快的更新频率
+ private LatLng wifiLocation = null;
+ private long lastWifiUpdateTime = 0;
+ private static final long WIFI_DATA_EXPIRY = 5000; // WiFi数据5秒内有效
+
+ // EKF状态权重
+ private static final float GNSS_WEIGHT = 0.35f;
+ private static final float PDR_WEIGHT = 0.45f;
+ private static final float WIFI_WEIGHT = 0.20f; // 降低WiFi权重
+
+ // 步伐检测相关变量
+ private long lastStepTime = 0;
+ private static final long MIN_STEP_INTERVAL = 300; // 降低最小步伐间隔
+ private static final float STEP_THRESHOLD = 0.30f; // 调整步伐峰值阈值
+ private boolean isAscending = false;
+ private double lastPeakValue = 0;
+ private final double[] recentPeaks = new double[5]; // 增加峰值历史记录
+ private int peakIndex = 0;
+
+ // 轨迹平滑相关变量
+ private static final int TRAJECTORY_WINDOW_SIZE = 5;
+ private final float[] latitudeWindow = new float[TRAJECTORY_WINDOW_SIZE];
+ private final float[] longitudeWindow = new float[TRAJECTORY_WINDOW_SIZE];
+ private int windowIndex = 0;
+ private boolean windowFull = false;
+
+ // 保存最后的PDR位置用于融合
+ private float lastPdrLatitude = 0;
+ private float lastPdrLongitude = 0;
+
+ // 添加保存融合位置的成员变量
+ private LatLng currentEkfPosition = null;
+
//region Initialisation
/**
* Private constructor for implementing singleton design pattern for SensorFusion.
@@ -191,6 +278,16 @@ private SensorFusion() {
this.R = new float[9];
// GNSS initial Long-Lat array
this.startLocation = new float[2];
+
+ // 初始化位置变量为0
+ this.latitude = 0.0f;
+ this.longitude = 0.0f;
+
+ // 初始化加速度大小列表
+ this.accelMagnitude = new ArrayList<>();
+
+ // 初始化气压值为设定的基准气压
+ this.pressure = STANDARD_PRESSURE;
}
@@ -218,9 +315,8 @@ public static SensorFusion getInstance() {
* @see WifiDataProcessor for network data processing.
*/
public void setContext(Context context) {
- this.appContext = context.getApplicationContext(); // store app context for later use
-
- // Initialise data collection devices (unchanged)...
+ this.context = context;
+ // Initialise data collection devices
this.accelerometerSensor = new MovementSensor(context, Sensor.TYPE_ACCELEROMETER);
this.barometerSensor = new MovementSensor(context, Sensor.TYPE_PRESSURE);
this.gyroscopeSensor = new MovementSensor(context, Sensor.TYPE_GYROSCOPE);
@@ -234,33 +330,39 @@ public void setContext(Context context) {
// Listener based devices
this.wifiProcessor = new WifiDataProcessor(context);
wifiProcessor.registerObserver(this);
- this.gnssProcessor = new GNSSDataProcessor(context, locationListener);
+ this.gnssProcessor = new GNSSDataProcessor(context,locationListener);
// Create object handling HTTPS communication
this.serverCommunications = new ServerCommunications(context);
// Save absolute and relative start time
this.absoluteStartTime = System.currentTimeMillis();
- this.bootTime = SystemClock.uptimeMillis();
- // Initialise saveRecording to false
+ this.bootTime = android.os.SystemClock.uptimeMillis();
+ // Initialise saveRecording to false - only record when explicitly started.
this.saveRecording = false;
- // Other initialisations...
+ // Over time data holder
this.accelMagnitude = new ArrayList<>();
+ // PDR
this.pdrProcessing = new PdrProcessing(context);
+ //Settings
this.settings = PreferenceManager.getDefaultSharedPreferences(context);
+
this.pathView = new PathView(context, null);
- this.wiFiPositioning = new WiFiPositioning(context);
+ // Initialising WiFi Positioning object
+ this.wiFiPositioning=new WiFiPositioning(context);
if(settings.getBoolean("overwrite_constants", false)) {
- this.filter_coefficient = Float.parseFloat(settings.getString("accel_filter", "0.96"));
- } else {
- this.filter_coefficient = FILTER_COEFFICIENT;
+ this.filter_coefficient =Float.parseFloat(settings.getString("accel_filter", "0.96"));
}
+ else {this.filter_coefficient = FILTER_COEFFICIENT;}
- // Keep app awake during the recording (using stored appContext)
- PowerManager powerManager = (PowerManager) this.appContext.getSystemService(Context.POWER_SERVICE);
- wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp::MyWakelockTag");
- }
+ // Keep app awake during the recording
+ PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+ "MyApp::MyWakelockTag");
+ // 初始化位置记录器
+ locationLogger = new LocationLogger(context);
+ }
//endregion
//region Sensor processing
@@ -275,78 +377,100 @@ public void setContext(Context context) {
*/
@Override
public void onSensorChanged(SensorEvent sensorEvent) {
- long currentTime = System.currentTimeMillis(); // Current time in milliseconds
- int sensorType = sensorEvent.sensor.getType();
-
- // Get the previous timestamp for this sensor type
- Long lastTimestamp = lastEventTimestamps.get(sensorType);
-
- if (lastTimestamp != null) {
- long timeGap = currentTime - lastTimestamp;
-
-// // Log a warning if the time gap is larger than the threshold
-// if (timeGap > LARGE_GAP_THRESHOLD_MS) {
-// Log.e("SensorFusion", "Large time gap detected for sensor " + sensorType +
-// " | Time gap: " + timeGap + " ms");
-// }
- }
-
- // Update timestamp and frequency counter for this sensor
- lastEventTimestamps.put(sensorType, currentTime);
- eventCounts.put(sensorType, eventCounts.getOrDefault(sensorType, 0) + 1);
-
-
-
- switch (sensorType) {
+ switch (sensorEvent.sensor.getType()) {
case Sensor.TYPE_ACCELEROMETER:
+ // 保存原始加速度数据
acceleration[0] = sensorEvent.values[0];
acceleration[1] = sensorEvent.values[1];
acceleration[2] = sensorEvent.values[2];
+
+ // 使用低通滤波器获取重力分量
+ gravity[0] = filter_coefficient * gravity[0] + (1 - filter_coefficient) * acceleration[0];
+ gravity[1] = filter_coefficient * gravity[1] + (1 - filter_coefficient) * acceleration[1];
+ gravity[2] = filter_coefficient * gravity[2] + (1 - filter_coefficient) * acceleration[2];
+
+ // 通过减去重力获得线性加速度
+ filteredAcc[0] = acceleration[0] - gravity[0];
+ filteredAcc[1] = acceleration[1] - gravity[1];
+ filteredAcc[2] = acceleration[2] - gravity[2];
+
+ // 计算合加速度大小
+ double accMagnitude = Math.sqrt(Math.pow(filteredAcc[0], 2) +
+ Math.pow(filteredAcc[1], 2) +
+ Math.pow(filteredAcc[2], 2));
+
+ // 保存加速度大小到列表中,用于步长估计
+ this.accelMagnitude.add(accMagnitude);
+
+ // 手动步伐检测逻辑 - 基于加速度峰值
+ manualStepDetection(accMagnitude);
+
break;
case Sensor.TYPE_PRESSURE:
- pressure = (1 - ALPHA) * pressure + ALPHA * sensorEvent.values[0];
- if (saveRecording) {
- this.elevation = pdrProcessing.updateElevation(
- SensorManager.getAltitude(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, pressure)
- );
+ // 使用移动平均值平滑气压数据
+ float smoothedPressure = getSmoothedPressure(sensorEvent.values[0]);
+ pressure = (1- ALPHA) * pressure + ALPHA * smoothedPressure;
+
+ // 计算海拔高度
+ float altitude = SensorManager.getAltitude(STANDARD_PRESSURE, pressure) - ALTITUDE_OFFSET;
+
+ // 添加更详细的调试日志
+ Log.d("PRESSURE_DEBUG", String.format(
+ "原始气压: %.2f hPa, 过滤后气压: %.2f hPa, 计算海拔: %.2f m, 位置: (%.6f, %.6f)",
+ sensorEvent.values[0],
+ pressure,
+ altitude,
+ latitude,
+ longitude
+ ));
+
+ // 更新海拔高度
+ this.elevation = pdrProcessing.updateElevation(altitude);
+
+ // 使用新的楼层计算方法
+ int newFloor = calculateFloor(altitude);
+
+ // 如果楼层发生变化,通知观察者
+ if (newFloor != currentFloor) {
+ currentFloor = newFloor;
+ Log.d("FLOOR_CHANGE", String.format(
+ "楼层变化 - 海拔: %.2f m, 新楼层: %d, 楼层高度: %.1f m, 特殊建筑: %b",
+ altitude,
+ currentFloor,
+ currentFloorHeight,
+ isInSpecialBuilding
+ ));
+ notifyFloorObservers(currentFloor);
}
break;
case Sensor.TYPE_GYROSCOPE:
+ // Gyro processing
+ //Store gyroscope readings
angularVelocity[0] = sensorEvent.values[0];
angularVelocity[1] = sensorEvent.values[1];
angularVelocity[2] = sensorEvent.values[2];
+ break;
+
case Sensor.TYPE_LINEAR_ACCELERATION:
+ // Acceleration processing with gravity already removed
filteredAcc[0] = sensorEvent.values[0];
filteredAcc[1] = sensorEvent.values[1];
filteredAcc[2] = sensorEvent.values[2];
- // Compute magnitude & add to accelMagnitude
- double accelMagFiltered = Math.sqrt(
- Math.pow(filteredAcc[0], 2) +
- Math.pow(filteredAcc[1], 2) +
- Math.pow(filteredAcc[2], 2)
- );
+ double accelMagFiltered = Math.sqrt(Math.pow(acceleration[0], 2) +
+ Math.pow(acceleration[1], 2) + Math.pow(acceleration[2], 2));
this.accelMagnitude.add(accelMagFiltered);
-
-// // Debug logging
-// Log.v("SensorFusion",
-// "Added new linear accel magnitude: " + accelMagFiltered
-// + "; accelMagnitude size = " + accelMagnitude.size());
-
elevator = pdrProcessing.estimateElevator(gravity, filteredAcc);
break;
case Sensor.TYPE_GRAVITY:
+ // Gravity processing obtained from acceleration
gravity[0] = sensorEvent.values[0];
gravity[1] = sensorEvent.values[1];
gravity[2] = sensorEvent.values[2];
-
- // Possibly log gravity values if needed
- //Log.v("SensorFusion", "Gravity: " + Arrays.toString(gravity));
-
elevator = pdrProcessing.estimateElevator(gravity, filteredAcc);
break;
@@ -359,71 +483,88 @@ public void onSensorChanged(SensorEvent sensorEvent) {
break;
case Sensor.TYPE_MAGNETIC_FIELD:
+ //Store magnetic field readings
magneticField[0] = sensorEvent.values[0];
magneticField[1] = sensorEvent.values[1];
magneticField[2] = sensorEvent.values[2];
break;
case Sensor.TYPE_ROTATION_VECTOR:
+ // Save values
this.rotation = sensorEvent.values.clone();
float[] rotationVectorDCM = new float[9];
- SensorManager.getRotationMatrixFromVector(rotationVectorDCM, this.rotation);
+ SensorManager.getRotationMatrixFromVector(rotationVectorDCM,this.rotation);
SensorManager.getOrientation(rotationVectorDCM, this.orientation);
break;
case Sensor.TYPE_STEP_DETECTOR:
- long stepTime = SystemClock.uptimeMillis() - bootTime;
-
-
- if (currentTime - lastStepTime < 20) {
- Log.e("SensorFusion", "Ignoring step event, too soon after last step event:" + (currentTime - lastStepTime) + " ms");
- // Ignore rapid successive step events
- break;
+ // 当前时间
+ long currentTime = System.currentTimeMillis();
+ long stepTime = android.os.SystemClock.uptimeMillis() - bootTime;
+
+ // 检查距离上一次步伐检测的时间间隔,过滤掉过快的步伐
+ if (currentTime - lastStepTime < MIN_STEP_INTERVAL) {
+ Log.d("SensorFusion", "忽略过快的步伐检测,间隔" + (currentTime - lastStepTime) + "ms < " + MIN_STEP_INTERVAL + "ms");
+ break; // 忽略此次步伐检测
}
-
- else {
- lastStepTime = currentTime;
- // Log if accelMagnitude is empty
- if (accelMagnitude.isEmpty()) {
- Log.e("SensorFusion",
- "stepDetection triggered, but accelMagnitude is empty! " +
- "This can cause updatePdr(...) to fail or return bad results.");
- } else {
- Log.d("SensorFusion",
- "stepDetection triggered, accelMagnitude size = " + accelMagnitude.size());
- }
-
- float[] newCords = this.pdrProcessing.updatePdr(
- stepTime,
- this.accelMagnitude,
- this.orientation[0]
+
+ // 检查加速度幅值是否足够,过滤小幅度振动
+ double maxAccel = 0;
+ for (double acc : accelMagnitude) {
+ maxAccel = Math.max(maxAccel, acc);
+ }
+
+ if (maxAccel < STEP_THRESHOLD * 0.7) {
+ Log.d("SensorFusion", "忽略微小振动引起的步伐检测,最大加速度" + maxAccel + " < " + (STEP_THRESHOLD * 0.7));
+ break; // 忽略此次步伐检测
+ }
+
+ // 更新PDR位置
+ float[] newCords = this.pdrProcessing.updatePdr(stepTime, this.accelMagnitude, this.orientation[0]);
+
+ // 添加详细日志,帮助调试步数检测
+ Log.d("SensorFusion", "步伐检测触发 - 时间: " + stepTime +
+ "ms, 位置变化: [" + newCords[0] + ", " + newCords[1] + "], 间隔: " + (currentTime - lastStepTime) + "ms");
+
+ if (saveRecording) {
+ // Store the PDR coordinates for plotting the trajectory
+ this.pathView.drawTrajectory(newCords);
+ }
+ this.accelMagnitude.clear();
+ if (saveRecording) {
+ stepCounter++;
+ trajectory.addPdrData(Traj.Pdr_Sample.newBuilder()
+ .setRelativeTimestamp(android.os.SystemClock.uptimeMillis() - bootTime)
+ .setX(newCords[0]).setY(newCords[1]));
+ }
+
+ // 检测到步伐后马上记录到位置日志器
+ if (saveRecording && locationLogger != null) {
+ // 使用PDR位置更新LocationLogger
+ float[] pdrLongLat = getPdrLongLat(newCords[0], newCords[1]);
+
+ // 保存最后的PDR位置用于EKF融合
+ lastPdrLatitude = pdrLongLat[0];
+ lastPdrLongitude = pdrLongLat[1];
+
+ locationLogger.logLocation(
+ currentTime,
+ pdrLongLat[0],
+ pdrLongLat[1]
);
-
- // Clear the accelMagnitude after using it
- this.accelMagnitude.clear();
-
-
- if (saveRecording) {
- this.pathView.drawTrajectory(newCords);
- stepCounter++;
- trajectory.addPdrData(Traj.Pdr_Sample.newBuilder()
- .setRelativeTimestamp(SystemClock.uptimeMillis() - bootTime)
- .setX(newCords[0])
- .setY(newCords[1]));
- }
- break;
+
+ // 添加PDR轨迹点
+ addTrajectoryPoint(pdrLongLat[0], pdrLongLat[1]);
+
+ // 不需要在这里直接生成EKF位置 - 现在由定时器统一处理
+
+ Log.d("SensorFusion", "步伐检测 - 已记录PDR位置: lat=" +
+ pdrLongLat[0] + ", lng=" + pdrLongLat[1]);
}
-
- }
- }
-
- /**
- * Utility function to log the event frequency of each sensor.
- * Call this periodically for debugging purposes.
- */
- public void logSensorFrequencies() {
- for (int sensorType : eventCounts.keySet()) {
- Log.d("SensorFusion", "Sensor " + sensorType + " | Event Count: " + eventCounts.get(sensorType));
+
+ // 更新最后一次步伐时间
+ lastStepTime = currentTime;
+ break;
}
}
@@ -437,23 +578,59 @@ public void logSensorFrequencies() {
*/
class myLocationListener implements LocationListener{
@Override
- public void onLocationChanged(@NonNull Location location) {
- //Toast.makeText(context, "Location Changed", Toast.LENGTH_SHORT).show();
- latitude = (float) location.getLatitude();
- longitude = (float) location.getLongitude();
- float altitude = (float) location.getAltitude();
- float accuracy = (float) location.getAccuracy();
- float speed = (float) location.getSpeed();
- String provider = location.getProvider();
- if(saveRecording) {
- trajectory.addGnssData(Traj.GNSS_Sample.newBuilder()
- .setAccuracy(accuracy)
- .setAltitude(altitude)
- .setLatitude(latitude)
- .setLongitude(longitude)
- .setSpeed(speed)
- .setProvider(provider)
- .setRelativeTimestamp(System.currentTimeMillis()-absoluteStartTime));
+ public void onLocationChanged(Location location) {
+ if(location != null){
+ latitude = (float) location.getLatitude();
+ longitude = (float) location.getLongitude();
+
+ // 保存最后GNSS更新时间
+ lastGnssUpdateTime = System.currentTimeMillis();
+
+ // 记录位置到日志
+ if(saveRecording && locationLogger != null) {
+ locationLogger.logLocation(
+ lastGnssUpdateTime,
+ latitude,
+ longitude
+ );
+
+ // 记录GNSS位置
+ locationLogger.logGnssLocation(
+ lastGnssUpdateTime,
+ latitude,
+ longitude
+ );
+
+ // 添加轨迹点
+ addTrajectoryPoint(latitude, longitude);
+
+ // 不需要在这里直接生成EKF位置 - 现在由定时器统一处理
+ }
+
+ // 添加详细的日志
+ Log.d("LOCATION_UPDATE", String.format(
+ "位置更新 - 提供者: %s, 纬度: %.6f, 经度: %.6f, 精度: %.1f米",
+ location.getProvider(),
+ latitude,
+ longitude,
+ location.getAccuracy()
+ ));
+
+ if(saveRecording) {
+ float altitude = (float) location.getAltitude();
+ float accuracy = (float) location.getAccuracy();
+ float speed = (float) location.getSpeed();
+ String provider = location.getProvider();
+
+ trajectory.addGnssData(Traj.GNSS_Sample.newBuilder()
+ .setAccuracy(accuracy)
+ .setAltitude(altitude)
+ .setLatitude(latitude)
+ .setLongitude(longitude)
+ .setSpeed(speed)
+ .setProvider(provider)
+ .setRelativeTimestamp(System.currentTimeMillis()-absoluteStartTime));
+ }
}
}
}
@@ -472,16 +649,17 @@ public void update(Object[] wifiList) {
if(this.saveRecording) {
Traj.WiFi_Sample.Builder wifiData = Traj.WiFi_Sample.newBuilder()
- .setRelativeTimestamp(SystemClock.uptimeMillis()-bootTime);
+ .setRelativeTimestamp(android.os.SystemClock.uptimeMillis()-bootTime);
for (Wifi data : this.wifiList) {
wifiData.addMacScans(Traj.Mac_Scan.newBuilder()
- .setRelativeTimestamp(SystemClock.uptimeMillis() - bootTime)
+ .setRelativeTimestamp(android.os.SystemClock.uptimeMillis() - bootTime)
.setMac(data.getBssid()).setRssi(data.getLevel()));
}
// Adding WiFi data to Trajectory
this.trajectory.addWifiData(wifiData);
}
- createWifiPositioningRequest();
+ // 使用带回调的WiFi请求方法,而不是原来的
+ createWifiPositionRequestCallback();
}
/**
@@ -524,12 +702,19 @@ private void createWifiPositionRequestCallback(){
this.wiFiPositioning.request(wifiFingerPrint, new WiFiPositioning.VolleyCallback() {
@Override
public void onSuccess(LatLng wifiLocation, int floor) {
- // Handle the success response
+ // 更新WiFi位置和最后更新时间
+ SensorFusion.this.wifiLocation = wifiLocation;
+ lastWifiUpdateTime = System.currentTimeMillis();
+
+ Log.d("WIFI_LOCATION", String.format(
+ "接收WiFi位置更新: [%.6f, %.6f], 楼层: %d",
+ wifiLocation.latitude, wifiLocation.longitude, floor));
}
@Override
public void onError(String message) {
- // Handle the error response
+ // 记录错误
+ Log.e("WIFI_LOCATION", "WiFi定位错误: " + message);
}
});
} catch (JSONException e) {
@@ -635,7 +820,7 @@ public void onAccuracyChanged(Sensor sensor, int i) {}
* @return longitude and latitude data in a float[2].
*/
public float[] getGNSSLatitude(boolean start) {
- float [] latLong = new float[2];
+ float[] latLong = new float[2];
if(!start) {
latLong[0] = latitude;
latLong[1] = longitude;
@@ -780,6 +965,34 @@ public int getHoldMode(){
}
}
+ /**
+ * 获取当前估计的楼层
+ * @return 当前楼层数(0表示地面层)
+ */
+ public int getCurrentFloor() {
+ return currentFloor;
+ }
+
+ /**
+ * 校准当前位置的基准气压值
+ * @param newPressure 新的基准气压值 (hPa)
+ */
+ public void calibrateBasePressure(float newPressure) {
+ STANDARD_PRESSURE = newPressure;
+ Log.d("PRESSURE_CALIBRATE", String.format(
+ "基准气压已更新为: %.2f hPa",
+ STANDARD_PRESSURE
+ ));
+ }
+
+ /**
+ * 获取当前基准气压值
+ * @return 当前基准气压值 (hPa)
+ */
+ public float getBasePressure() {
+ return STANDARD_PRESSURE;
+ }
+
//endregion
//region Start/Stop
@@ -795,18 +1008,27 @@ public int getHoldMode(){
* @see GNSSDataProcessor handles location data.
*/
public void resumeListening() {
- accelerometerSensor.sensorManager.registerListener(this, accelerometerSensor.sensor, 10000, (int) maxReportLatencyNs);
- accelerometerSensor.sensorManager.registerListener(this, linearAccelerationSensor.sensor, 10000, (int) maxReportLatencyNs);
- accelerometerSensor.sensorManager.registerListener(this, gravitySensor.sensor, 10000, (int) maxReportLatencyNs);
+ // 将IMU相关传感器的采样频率设置为5000微秒(200Hz)
+ accelerometerSensor.sensorManager.registerListener(this, accelerometerSensor.sensor, 5000);
+ accelerometerSensor.sensorManager.registerListener(this, linearAccelerationSensor.sensor, 5000);
+ accelerometerSensor.sensorManager.registerListener(this, gravitySensor.sensor, 5000);
barometerSensor.sensorManager.registerListener(this, barometerSensor.sensor, (int) 1e6);
- gyroscopeSensor.sensorManager.registerListener(this, gyroscopeSensor.sensor, 10000, (int) maxReportLatencyNs);
+ gyroscopeSensor.sensorManager.registerListener(this, gyroscopeSensor.sensor, 5000);
lightSensor.sensorManager.registerListener(this, lightSensor.sensor, (int) 1e6);
proximitySensor.sensorManager.registerListener(this, proximitySensor.sensor, (int) 1e6);
- magnetometerSensor.sensorManager.registerListener(this, magnetometerSensor.sensor, 10000, (int) maxReportLatencyNs);
- stepDetectionSensor.sensorManager.registerListener(this, stepDetectionSensor.sensor, SensorManager.SENSOR_DELAY_NORMAL);
+ magnetometerSensor.sensorManager.registerListener(this, magnetometerSensor.sensor, 5000);
+
+ // 降低步伐检测器的采样率,从SENSOR_DELAY_FASTEST改为SensorManager.SENSOR_DELAY_GAME
+ // 这样可以减少过度灵敏的问题
+ stepDetectionSensor.sensorManager.registerListener(this, stepDetectionSensor.sensor, SensorManager.SENSOR_DELAY_GAME);
+
rotationSensor.sensorManager.registerListener(this, rotationSensor.sensor, (int) 1e6);
wifiProcessor.startListening();
- gnssProcessor.startLocationUpdates();
+
+ // 确保GNSS处理器启动
+ if (gnssProcessor != null) {
+ gnssProcessor.startLocationUpdates();
+ }
}
/**
@@ -834,12 +1056,14 @@ public void stopListening() {
//The app often crashes here because the scan receiver stops after it has found the list.
// It will only unregister one if there is to unregister
try {
- this.wifiProcessor.stopListening(); //error here?
+ this.wifiProcessor.stopListening();
} catch (Exception e) {
System.err.println("Wifi resumed before existing");
}
// Stop receiving location updates
- this.gnssProcessor.stopUpdating();
+ if (gnssProcessor != null) {
+ gnssProcessor.stopUpdating();
+ }
}
}
@@ -852,37 +1076,40 @@ public void stopListening() {
* @see Traj object for storing data.
*/
public void startRecording() {
- // If wakeLock is null (e.g. not initialized or was cleared), reinitialize it.
- if (wakeLock == null) {
- PowerManager powerManager = (PowerManager) this.appContext.getSystemService(Context.POWER_SERVICE);
- wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp::MyWakelockTag");
- }
- wakeLock.acquire(31 * 60 * 1000L /*31 minutes*/);
-
+ // Acquire wakelock so the phone will record with a locked screen. Timeout after 31 minutes.
+ this.wakeLock.acquire(31*60*1000L /*31 minutes*/);
this.saveRecording = true;
this.stepCounter = 0;
this.absoluteStartTime = System.currentTimeMillis();
- this.bootTime = SystemClock.uptimeMillis();
+ this.bootTime = android.os.SystemClock.uptimeMillis();
+
+ // 清空轨迹点列表
+ trajectoryPoints.clear();
+
+ // 启动定时EKF融合更新
+ startEkfTimer();
+
// Protobuf trajectory class for sending sensor data to restful API
this.trajectory = Traj.Trajectory.newBuilder()
.setAndroidVersion(Build.VERSION.RELEASE)
.setStartTimestamp(absoluteStartTime)
+ /*.addApsData(Traj.AP_Data.newBuilder().setMac(example_mac).setSsid(example_ssid)
+ .setFrequency(example_frequency))*/
.setAccelerometerInfo(createInfoBuilder(accelerometerSensor))
.setGyroscopeInfo(createInfoBuilder(gyroscopeSensor))
.setMagnetometerInfo(createInfoBuilder(magnetometerSensor))
.setBarometerInfo(createInfoBuilder(barometerSensor))
.setLightSensorInfo(createInfoBuilder(lightSensor));
-
-
-
this.storeTrajectoryTimer = new Timer();
- this.storeTrajectoryTimer.schedule(new storeDataInTrajectory(), 0, TIME_CONST);
+ this.storeTrajectoryTimer.scheduleAtFixedRate(new storeDataInTrajectory(), 0, TIME_CONST);
this.pdrProcessing.resetPDR();
if(settings.getBoolean("overwrite_constants", false)) {
this.filter_coefficient = Float.parseFloat(settings.getString("accel_filter", "0.96"));
- } else {
- this.filter_coefficient = FILTER_COEFFICIENT;
}
+ else {this.filter_coefficient = FILTER_COEFFICIENT;}
+
+ // 初始化位置记录器
+ locationLogger = new LocationLogger(context);
}
/**
@@ -892,13 +1119,21 @@ public void startRecording() {
* the timer objects.
*
* @see Traj object for storing data.
- * @see SettingsFragment navigation that might cancel recording.
+ * @see com.openpositioning.PositionMe.fragments.SettingsFragment navigation that might cancel recording.
*/
public void stopRecording() {
// Only cancel if we are running
if(this.saveRecording) {
this.saveRecording = false;
storeTrajectoryTimer.cancel();
+
+ // 停止EKF融合定时器
+ stopEkfTimer();
+
+ // 保存位置日志
+ if (locationLogger != null) {
+ locationLogger.saveToFile();
+ }
}
if(wakeLock.isHeld()) {
this.wakeLock.release();
@@ -907,6 +1142,164 @@ public void stopRecording() {
//endregion
+ // 定时执行EKF融合的定时器
+ // private Timer ekfTimer;
+ // private static final int EKF_UPDATE_INTERVAL = 200; // 更快的更新频率
+
+ /**
+ * 重写的EKF融合算法 - 每次更新时融合所有可用数据源
+ */
+ private class updateEkfLocation extends TimerTask {
+ @Override
+ public void run() {
+ try {
+ if (!saveRecording || locationLogger == null) return;
+
+ long currentTime = System.currentTimeMillis();
+
+ // 检查是否有足够的数据源
+ boolean hasPdr = lastPdrLatitude != 0 && lastPdrLongitude != 0;
+ boolean hasGnss = latitude != 0 && longitude != 0;
+ boolean hasWifi = wifiLocation != null &&
+ (currentTime - lastWifiUpdateTime < WIFI_DATA_EXPIRY);
+
+ // 至少需要一个数据源
+ if (!hasPdr && !hasGnss && !hasWifi) {
+ Log.d("EKF_FUSION", "没有可用的数据源进行融合");
+ return;
+ }
+
+ // 初始化融合位置
+ float fusedLat = 0;
+ float fusedLon = 0;
+ float totalWeight = 0;
+
+ // 融合PDR数据
+ if (hasPdr) {
+ float pdrWeight = PDR_WEIGHT;
+ // 计算PDR数据的时间衰减
+ long pdrTimeDiff = currentTime - lastStepTime;
+ if (pdrTimeDiff > 2000) {
+ // 步数数据超过2秒,权重线性衰减
+ pdrWeight *= Math.max(0.3f, 1.0f - (pdrTimeDiff - 2000) / 8000.0f);
+ }
+
+ fusedLat += pdrWeight * lastPdrLatitude;
+ fusedLon += pdrWeight * lastPdrLongitude;
+ totalWeight += pdrWeight;
+
+ Log.d("EKF_FUSION", String.format(
+ "PDR数据: [%.6f, %.6f], 权重: %.2f",
+ lastPdrLatitude, lastPdrLongitude, pdrWeight));
+ }
+
+ // 融合GNSS数据
+ if (hasGnss) {
+ float gnssWeight = GNSS_WEIGHT;
+ // 计算GNSS数据的时间衰减
+ long gnssTimeDiff = currentTime - lastGnssUpdateTime;
+ if (gnssTimeDiff > 2000) {
+ // GNSS数据超过2秒,权重线性衰减
+ gnssWeight *= Math.max(0.3f, 1.0f - (gnssTimeDiff - 2000) / 8000.0f);
+ }
+
+ fusedLat += gnssWeight * latitude;
+ fusedLon += gnssWeight * longitude;
+ totalWeight += gnssWeight;
+
+ Log.d("EKF_FUSION", String.format(
+ "GNSS数据: [%.6f, %.6f], 权重: %.2f",
+ latitude, longitude, gnssWeight));
+ }
+
+ // 融合WiFi数据
+ if (hasWifi) {
+ float wifiWeight = WIFI_WEIGHT;
+ // 计算WiFi数据的时间衰减
+ long wifiTimeDiff = currentTime - lastWifiUpdateTime;
+ if (wifiTimeDiff > 3000) {
+ // WiFi数据超过3秒,权重线性衰减
+ wifiWeight *= Math.max(0.3f, 1.0f - (wifiTimeDiff - 3000) / 7000.0f);
+ }
+
+ fusedLat += wifiWeight * (float)wifiLocation.latitude;
+ fusedLon += wifiWeight * (float)wifiLocation.longitude;
+ totalWeight += wifiWeight;
+
+ Log.d("EKF_FUSION", String.format(
+ "WiFi数据: [%.6f, %.6f], 权重: %.2f",
+ wifiLocation.latitude, wifiLocation.longitude, wifiWeight));
+ }
+
+ // 归一化权重
+ if (totalWeight > 0) {
+ fusedLat /= totalWeight;
+ fusedLon /= totalWeight;
+ } else if (hasPdr) {
+ // 如果权重归一化出问题,但有PDR数据,使用PDR数据
+ fusedLat = lastPdrLatitude;
+ fusedLon = lastPdrLongitude;
+ } else if (hasGnss) {
+ // 否则使用GNSS数据
+ fusedLat = latitude;
+ fusedLon = longitude;
+ } else if (hasWifi) {
+ // 最后选择WiFi数据
+ fusedLat = (float)wifiLocation.latitude;
+ fusedLon = (float)wifiLocation.longitude;
+ }
+
+ // 保存当前EKF融合位置到成员变量
+ currentEkfPosition = new LatLng(fusedLat, fusedLon);
+
+ // 记录融合位置
+ locationLogger.logEkfLocation(currentTime, fusedLat, fusedLon);
+
+ // 添加到轨迹点列表
+ addTrajectoryPoint(fusedLat, fusedLon);
+
+ Log.d("EKF_FUSION", String.format(
+ "融合位置: [%.6f, %.6f], 总权重: %.2f",
+ fusedLat, fusedLon, totalWeight));
+
+ // 不再需要递归调度,已使用scheduleAtFixedRate
+ } catch (Exception e) {
+ Log.e("EKF_FUSION", "融合位置更新出错: " + e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * 启动EKF融合定时器
+ */
+ private void startEkfTimer() {
+ Log.d("EKF_FUSION", "启动EKF融合定时器");
+
+ if (ekfTimer != null) {
+ ekfTimer.cancel();
+ }
+ ekfTimer = new Timer("EKF-Fusion-Timer");
+
+ // 使用scheduleAtFixedRate而不是递归调度
+ ekfTimer.scheduleAtFixedRate(new updateEkfLocation(), 0, EKF_UPDATE_INTERVAL);
+ }
+
+ /**
+ * 停止EKF融合定时器
+ */
+ private void stopEkfTimer() {
+ if (ekfTimer != null) {
+ Log.d("EKF_FUSION", "停止EKF融合定时器");
+ ekfTimer.cancel();
+ ekfTimer = null;
+ }
+ }
+
+ // 保存GNSS最后更新时间
+ private long lastGnssUpdateTime = 0;
+
+ //endregion
+
//region Trajectory object
/**
@@ -949,66 +1342,369 @@ private Traj.Sensor_Info.Builder createInfoBuilder(MovementSensor sensor) {
private class storeDataInTrajectory extends TimerTask {
public void run() {
// Store IMU and magnetometer data in Trajectory class
- trajectory.addImuData(Traj.Motion_Sample.newBuilder()
- .setRelativeTimestamp(SystemClock.uptimeMillis()-bootTime)
- .setAccX(acceleration[0])
- .setAccY(acceleration[1])
- .setAccZ(acceleration[2])
- .setGyrX(angularVelocity[0])
- .setGyrY(angularVelocity[1])
- .setGyrZ(angularVelocity[2])
- .setGyrZ(angularVelocity[2])
- .setRotationVectorX(rotation[0])
- .setRotationVectorY(rotation[1])
- .setRotationVectorZ(rotation[2])
- .setRotationVectorW(rotation[3])
- .setStepCount(stepCounter))
- .addPositionData(Traj.Position_Sample.newBuilder()
- .setMagX(magneticField[0])
- .setMagY(magneticField[1])
- .setMagZ(magneticField[2])
- .setRelativeTimestamp(SystemClock.uptimeMillis()-bootTime))
-// .addGnssData(Traj.GNSS_Sample.newBuilder()
-// .setLatitude(latitude)
-// .setLongitude(longitude)
-// .setRelativeTimestamp(SystemClock.uptimeMillis()-bootTime))
- ;
-
- // Divide timer with a counter for storing data every 1 second
- if (counter == 99) {
- counter = 0;
- // Store pressure and light data
- if (barometerSensor.sensor != null) {
- trajectory.addPressureData(Traj.Pressure_Sample.newBuilder()
- .setPressure(pressure)
- .setRelativeTimestamp(SystemClock.uptimeMillis() - bootTime))
- .addLightData(Traj.Light_Sample.newBuilder()
- .setLight(light)
- .setRelativeTimestamp(SystemClock.uptimeMillis() - bootTime)
- .build());
- }
+ try {
+ trajectory.addImuData(Traj.Motion_Sample.newBuilder()
+ .setRelativeTimestamp(android.os.SystemClock.uptimeMillis()-bootTime)
+ .setAccX(acceleration[0])
+ .setAccY(acceleration[1])
+ .setAccZ(acceleration[2])
+ .setGyrX(angularVelocity[0])
+ .setGyrY(angularVelocity[1])
+ .setGyrZ(angularVelocity[2])
+ .setRotationVectorX(rotation[0])
+ .setRotationVectorY(rotation[1])
+ .setRotationVectorZ(rotation[2])
+ .setRotationVectorW(rotation[3])
+ .setStepCount(stepCounter))
+ .addPositionData(Traj.Position_Sample.newBuilder()
+ .setMagX(magneticField[0])
+ .setMagY(magneticField[1])
+ .setMagZ(magneticField[2])
+ .setRelativeTimestamp(android.os.SystemClock.uptimeMillis()-bootTime));
+
+ // Divide timer with a counter for storing data every 1 second
+ if (counter == 99) {
+ counter = 0;
+ // Store pressure and light data
+ if (barometerSensor.sensor != null) {
+ trajectory.addPressureData(Traj.Pressure_Sample.newBuilder()
+ .setPressure(pressure)
+ .setRelativeTimestamp(android.os.SystemClock.uptimeMillis() - bootTime))
+ .addLightData(Traj.Light_Sample.newBuilder()
+ .setLight(light)
+ .setRelativeTimestamp(android.os.SystemClock.uptimeMillis() - bootTime)
+ .build());
+ }
- // Divide the timer for storing AP data every 5 seconds
- if (secondCounter == 4) {
- secondCounter = 0;
- //Current Wifi Object
- Wifi currentWifi = wifiProcessor.getCurrentWifiData();
- trajectory.addApsData(Traj.AP_Data.newBuilder()
- .setMac(currentWifi.getBssid())
- .setSsid(currentWifi.getSsid())
- .setFrequency(currentWifi.getFrequency()));
+ // Divide the timer for storing AP data every 5 seconds
+ if (secondCounter == 4) {
+ secondCounter = 0;
+ //Current Wifi Object
+ Wifi currentWifi = wifiProcessor.getCurrentWifiData();
+ trajectory.addApsData(Traj.AP_Data.newBuilder()
+ .setMac(currentWifi.getBssid())
+ .setSsid(currentWifi.getSsid())
+ .setFrequency((int)currentWifi.getFrequency()));
+ }
+ else {
+ secondCounter++;
+ }
}
else {
- secondCounter++;
+ counter++;
}
+ } catch (Exception e) {
+ Log.e("SensorFusion", "轨迹数据添加错误: " + e.getMessage());
+ }
+ }
+ }
+
+ //endregion
+
+ /**
+ * 注册观察者以接收楼层更新
+ */
+ public void registerFloorObserver(Observer observer) {
+ if (!floorObservers.contains(observer)) {
+ floorObservers.add(observer);
+ }
+ }
+
+ /**
+ * 移除楼层更新观察者
+ */
+ public void removeFloorObserver(Observer observer) {
+ floorObservers.remove(observer);
+ }
+
+ /**
+ * 通知所有观察者楼层变化
+ */
+ private void notifyFloorObservers(int floor) {
+ Log.d("FLOOR_NOTIFY", String.format(
+ "正在通知观察者楼层变化,观察者数量: %d, 新楼层: %d",
+ floorObservers.size(),
+ floor
+ ));
+
+ for (Observer observer : floorObservers) {
+ Log.d("FLOOR_NOTIFY", "通知观察者: " + observer.getClass().getName());
+ Object[] updateData = new Object[]{floor};
+ observer.update(updateData);
+ }
+ }
+
+ /**
+ * 根据当前位置计算楼层
+ * @param altitude 当前海拔高度
+ * @return 计算得到的楼层数
+ */
+ private int calculateFloor(float altitude) {
+ LatLng currentPosition = new LatLng(latitude, longitude);
+
+ // 检查是否在Nucleus大楼内
+ if (BuildingPolygon.inNucleus(currentPosition)) {
+ currentFloorHeight = IndoorMapManager.NUCLEUS_FLOOR_HEIGHT;
+ isInSpecialBuilding = true;
+
+ // 计算楼层(Nucleus的特殊规则)
+ int calculatedFloor = (int)Math.floor(altitude / currentFloorHeight);
+
+ // Nucleus的楼层对应关系:
+ // calculatedFloor: -1 0 1 2 3
+ // 实际显示: LG G 1 2 3
+ // 所以不需要额外调整,只需要限制范围
+
+ // 限制楼层范围(-1到3,对应LG到3楼)
+ return Math.min(Math.max(calculatedFloor, -1), 3);
+ }
+ // 检查是否在图书馆内
+ else if (BuildingPolygon.inLibrary(currentPosition)) {
+ currentFloorHeight = IndoorMapManager.LIBRARY_FLOOR_HEIGHT;
+ isInSpecialBuilding = true;
+
+ // 计算楼层(图书馆的特殊规则)
+ int calculatedFloor = (int)Math.floor(altitude / currentFloorHeight);
+
+ // 限制楼层范围(0到3,对应G到3楼)
+ return Math.min(Math.max(calculatedFloor, 0), 3);
+ }
+ else {
+ // 不在特殊建筑物内,使用默认规则
+ isInSpecialBuilding = false;
+ currentFloorHeight = DEFAULT_FLOOR_HEIGHT;
+
+ // 计算楼层
+ int calculatedFloor = (int)Math.floor(altitude / currentFloorHeight);
+
+ // 对于其他位置,确保从0开始(G层)
+ return Math.max(calculatedFloor, 0); // 0=G, 1=1, 2=2, ...
+ }
+ }
+
+ /**
+ * 获取当前楼层的显示文本
+ * @return 楼层显示文本(如"LG", "G", "1", "2"等)
+ */
+ public String getFloorDisplay() {
+ LatLng currentPosition = new LatLng(latitude, longitude);
+
+ if (BuildingPolygon.inNucleus(currentPosition)) {
+ // Nucleus的特殊显示规则
+ if (currentFloor == -1) {
+ return "LG";
+ } else if (currentFloor == 0) {
+ return "G";
+ } else {
+ return String.valueOf(currentFloor);
}
- else {
- counter++;
+ } else {
+ // 其他位置(包括图书馆)的显示规则
+ if (currentFloor == 0) {
+ return "G";
+ } else {
+ return String.valueOf(currentFloor);
}
+ }
+ }
+
+ /**
+ * 在已知楼层位置校准气压计
+ * @param knownFloor 当前已知的楼层(0=G, -1=LG, 1=1F, etc.)
+ */
+ public void calibrateAtKnownFloor(int knownFloor) {
+ // 获取当前位置
+ LatLng currentPosition = new LatLng(latitude, longitude);
+ float floorHeight;
+
+ // 根据位置确定楼层高度
+ if (BuildingPolygon.inNucleus(currentPosition)) {
+ floorHeight = IndoorMapManager.NUCLEUS_FLOOR_HEIGHT;
+ } else if (BuildingPolygon.inLibrary(currentPosition)) {
+ floorHeight = IndoorMapManager.LIBRARY_FLOOR_HEIGHT;
+ } else {
+ floorHeight = DEFAULT_FLOOR_HEIGHT;
+ }
+
+ // 计算理论高度
+ float expectedAltitude = knownFloor * floorHeight;
+
+ // 根据当前气压和已知高度,反向计算基准气压
+ float newStandardPressure = pressure * (float)Math.exp(expectedAltitude / -7400);
+
+ // 更新基准气压
+ STANDARD_PRESSURE = newStandardPressure;
+
+ Log.d("PRESSURE_CALIBRATE", String.format(
+ "在已知楼层%d校准 - 当前气压: %.2f hPa, 理论高度: %.2f m, 新基准气压: %.2f hPa",
+ knownFloor,
+ pressure,
+ expectedAltitude,
+ STANDARD_PRESSURE
+ ));
+ }
+
+ /**
+ * 使用移动平均值平滑气压数据
+ */
+ private static final int PRESSURE_WINDOW_SIZE = 10;
+ private final float[] pressureWindow = new float[PRESSURE_WINDOW_SIZE];
+ private int pressureWindowIndex = 0;
+ private boolean pressureWindowFull = false;
+
+ private float getSmoothedPressure(float rawPressure) {
+ // 更新滑动窗口
+ pressureWindow[pressureWindowIndex] = rawPressure;
+ pressureWindowIndex = (pressureWindowIndex + 1) % PRESSURE_WINDOW_SIZE;
+ if (pressureWindowIndex == 0) {
+ pressureWindowFull = true;
+ }
+ // 计算平均值
+ float sum = 0;
+ int count = pressureWindowFull ? PRESSURE_WINDOW_SIZE : pressureWindowIndex;
+ for (int i = 0; i < count; i++) {
+ sum += pressureWindow[i];
}
+ return sum / count;
}
- //endregion
+ // 添加获取经纬度的方法
+ /**
+ * 获取当前纬度
+ * @return 当前纬度值
+ */
+ public float getLatitude() {
+ return this.latitude;
+ }
+
+ /**
+ * 获取当前经度
+ * @return 当前经度值
+ */
+ public float getLongitude() {
+ return this.longitude;
+ }
+
+ // 更新manualStepDetection方法,移除EKF融合代码,由定时器统一处理
+ private void manualStepDetection(double accMagnitude) {
+ long currentTime = System.currentTimeMillis();
+
+ // 时间间隔检查,防止过于频繁的步伐检测
+ if (currentTime - lastStepTime < MIN_STEP_INTERVAL) {
+ return;
+ }
+
+ // 检测峰值变化
+ if (!isAscending && accMagnitude > lastPeakValue) {
+ isAscending = true;
+ } else if (isAscending && accMagnitude < lastPeakValue) {
+ // 检测到峰值下降
+
+ // 保存峰值用于连续性检查
+ recentPeaks[peakIndex] = lastPeakValue;
+ peakIndex = (peakIndex + 1) % recentPeaks.length;
+
+ // 检查峰值是否符合走路模式 - 连续三个峰值相对稳定且超过阈值
+ boolean isValidStep = lastPeakValue > STEP_THRESHOLD;
+
+ // 如果有足够的峰值历史,检查连续性
+ if (currentTime - lastStepTime > 2000) { // 如果超过2秒没有步伐,重置判断
+ isValidStep = isValidStep && lastPeakValue > STEP_THRESHOLD * 1.2; // 第一步要求更明显的峰值
+ }
+
+ if (isValidStep) {
+ // 检测到有效步伐
+ long stepTime = android.os.SystemClock.uptimeMillis() - bootTime;
+ Log.d("SensorFusion", String.format("手动步伐检测: 峰值=%.2f, 阈值=%.2f, 间隔=%d毫秒",
+ lastPeakValue, STEP_THRESHOLD, currentTime - lastStepTime));
+
+ // 使用与系统步伐检测器相同的更新逻辑
+ float[] newCords = this.pdrProcessing.updatePdr(stepTime, this.accelMagnitude, this.orientation[0]);
+
+ if (saveRecording) {
+ this.pathView.drawTrajectory(newCords);
+ stepCounter++;
+ trajectory.addPdrData(Traj.Pdr_Sample.newBuilder()
+ .setRelativeTimestamp(stepTime)
+ .setX(newCords[0]).setY(newCords[1]));
+ }
+
+ // 检测到步伐后记录到位置日志器
+ if (saveRecording && locationLogger != null) {
+ float[] pdrLongLat = getPdrLongLat(newCords[0], newCords[1]);
+
+ // 保存最后的PDR位置用于EKF融合
+ lastPdrLatitude = pdrLongLat[0];
+ lastPdrLongitude = pdrLongLat[1];
+
+ // 记录PDR位置
+ locationLogger.logLocation(
+ currentTime,
+ pdrLongLat[0],
+ pdrLongLat[1]
+ );
+
+ // 添加PDR轨迹点
+ addTrajectoryPoint(pdrLongLat[0], pdrLongLat[1]);
+
+ // 不需要在这里直接生成EKF位置 - 现在由定时器统一处理
+
+ Log.d("SensorFusion", "手动步伐检测 - 已记录位置: lat=" +
+ pdrLongLat[0] + ", lng=" + pdrLongLat[1]);
+ }
+
+ this.accelMagnitude.clear();
+ lastStepTime = currentTime;
+ }
+ isAscending = false;
+ }
+
+ lastPeakValue = accMagnitude;
+ }
+
+ /**
+ * 将PDR相对坐标转换为地理坐标
+ * @param x PDR X坐标
+ * @param y PDR Y坐标
+ * @return 经纬度坐标数组 [纬度, 经度]
+ */
+ public float[] getPdrLongLat(float x, float y) {
+ // 如果没有起始位置,使用第一个GNSS位置作为起始位置
+ if (startLocation[0] == 0 && startLocation[1] == 0) {
+ // 获取第一个GNSS位置
+ float[] firstGnssPos = getGNSSLatitude(true);
+ if (firstGnssPos != null && firstGnssPos.length >= 2 && firstGnssPos[0] != 0 && firstGnssPos[1] != 0) {
+ startLocation = firstGnssPos;
+ Log.d("SensorFusion", "使用第一个GNSS位置作为PDR起始位置: [" + startLocation[0] + ", " + startLocation[1] + "]");
+ } else {
+ Log.e("SensorFusion", "无法获取有效的GNSS起始位置");
+ return new float[]{latitude, longitude};
+ }
+ }
+
+ // 每度对应的距离(米)
+ double metersPerLatDegree = 111320.0; // 1度纬度大约等于111.32公里
+ // 经度则随纬度变化
+ double metersPerLngDegree = 111320.0 * Math.cos(Math.toRadians(startLocation[0]));
+
+ // 将米转换为经纬度增量
+ float latOffset = y / (float)metersPerLatDegree;
+ float lngOffset = x / (float)metersPerLngDegree;
+
+ // 从起始位置增加偏移
+ float resultLat = startLocation[0] + latOffset;
+ float resultLng = startLocation[1] + lngOffset;
+
+ return new float[]{resultLat, resultLng};
+ }
+
+ /**
+ * 获取当前EKF融合位置
+ * @return 当前EKF融合位置,如果未计算则返回null
+ */
+ public LatLng getEkfPosition() {
+ return currentEkfPosition;
+ }
}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorTypes.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorTypes.java
index ee3bbcc1..9fb4dcb5 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorTypes.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorTypes.java
@@ -1,12 +1,10 @@
package com.openpositioning.PositionMe.sensors;
-import com.openpositioning.PositionMe.presentation.fragment.MeasurementsFragment;
-
/**
* Enum of the sensor types.
*
* Simplified version of default Android Sensor.TYPE, with the order matching the table layout for
- * the {@link MeasurementsFragment}. Includes virtual sensors and other
+ * the {@link com.openpositioning.PositionMe.fragments.MeasurementsFragment}. Includes virtual sensors and other
* data providing devices as well as derived data.
*
* @author Mate Stodulka
@@ -20,5 +18,6 @@ public enum SensorTypes {
PRESSURE,
PROXIMITY,
GNSSLATLONG,
- PDR;
+ PDR,
+ WIFILATLONG;
}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/WiFiPositioning.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/WiFiPositioning.java
index dbf809dd..8e7c550a 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/sensors/WiFiPositioning.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/WiFiPositioning.java
@@ -1,181 +1,200 @@
package com.openpositioning.PositionMe.sensors;
+import android.Manifest;
import android.content.Context;
+import android.content.pm.PackageManager;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.os.Bundle;
+import android.os.Looper;
import android.util.Log;
-import com.android.volley.Request;
+import androidx.core.app.ActivityCompat;
+
import com.android.volley.RequestQueue;
-import com.android.volley.toolbox.JsonObjectRequest;
import com.android.volley.toolbox.Volley;
import com.google.android.gms.maps.model.LatLng;
-import org.json.JSONException;
import org.json.JSONObject;
/**
- * Class for creating and handling POST requests for obtaining the current position using
- * WiFi positioning API from https://openpositioning.org/api/position/fine
- *
- * The class creates POST requests based on WiFi fingerprints and obtains the user's location
- *
- * The request are handled asynchronously, The WiFi position coordinates and floor are updated
- * when the response of the POST request is obtained.
+ * Class for handling WiFi-based positioning using Android's LocationManager
+ *
+ * 由于OpenPositioning API没有提供WiFi定位端点,我们改用Android系统的LocationManager
+ * 获取基于WiFi的位置信息。
+ *
+ * 该类将使用Android的网络位置提供商(主要是WiFi和蜂窝网络)来获取位置信息。
*
- * One can create a POST request using the function provided in the class (createPostRequest()) with
- * the WiFi fingerprint
- * Its then added to the RequestQueue to be handled asynchronously (not blocking the main thread)
- * When the response to the request is obtained the wifiLocation and floor are updated.
- * Calling the getters for wifiLocation and the floor allows obtaining the WiFi location and floor
- * from the POST request response.
* @author Arun Gopalakrishnan
*/
public class WiFiPositioning {
- // Queue for storing the POST requests made
- private RequestQueue requestQueue;
- // URL for WiFi positioning API
- private static final String url="https://openpositioning.org/api/position/fine";
-
- /**
- * Getter for the WiFi positioning coordinates obtained using openpositioning API
- * @return the user's coordinates based on openpositioning API
- */
- public LatLng getWifiLocation() {
- return wifiLocation;
- }
-
- // Store user's location obtained using WiFi positioning
+ // 上下文和位置服务
+ private final Context context;
+ private final LocationManager locationManager;
+
+ // WiFi位置和楼层
private LatLng wifiLocation;
+ private int floor = 0; // 默认楼层为0
+
+ // 位置更新最小间隔(毫秒)和最小距离(米)
+ private static final long MIN_TIME = 1000; // 1秒
+ private static final float MIN_DISTANCE = 0; // 不限制最小距离
+
+ // 用于保存回调的队列
+ private final RequestQueue requestQueue;
+
/**
- * Getter for the WiFi positioning floor obtained using openpositioning API
- * @return the user's location based on openpositioning API
+ * 位置监听器,处理位置更新
*/
- public int getFloor() {
- return floor;
- }
+ private final LocationListener locationListener = new LocationListener() {
+ @Override
+ public void onLocationChanged(Location location) {
+ wifiLocation = new LatLng(location.getLatitude(), location.getLongitude());
+ Log.d("WiFiPositioning", "位置已更新: " + wifiLocation.latitude + ", " + wifiLocation.longitude);
+ }
+
+ @Override
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+ Log.d("WiFiPositioning", "提供商状态变化: " + provider + ", 状态: " + status);
+ }
- // Store current floor of user, default value 0 (ground floor)
- private int floor=0;
+ @Override
+ public void onProviderEnabled(String provider) {
+ Log.d("WiFiPositioning", "提供商已启用: " + provider);
+ }
+ @Override
+ public void onProviderDisabled(String provider) {
+ Log.d("WiFiPositioning", "提供商已禁用: " + provider);
+ }
+ };
/**
* Constructor to create the WiFi positioning object
*
- * Initialising a request queue to handle the POST requests asynchronously
+ * Initialising the location manager and request queue
*
* @param context Context of object calling
*/
- public WiFiPositioning(Context context){
- // Initialising the Request queue
+ public WiFiPositioning(Context context) {
+ this.context = context;
+ this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
this.requestQueue = Volley.newRequestQueue(context.getApplicationContext());
+
+ // 启动位置更新
+ startLocationUpdates();
+ }
+
+ /**
+ * 开始接收位置更新
+ */
+ private void startLocationUpdates() {
+ if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
+ Log.e("WiFiPositioning", "没有位置权限,无法获取位置更新");
+ return;
+ }
+
+ // 优先使用网络位置(WiFi和蜂窝网络)
+ if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
+ locationManager.requestLocationUpdates(
+ LocationManager.NETWORK_PROVIDER,
+ MIN_TIME,
+ MIN_DISTANCE,
+ locationListener,
+ Looper.getMainLooper()
+ );
+ Log.d("WiFiPositioning", "已请求网络位置更新");
+
+ // 尝试立即获取一个位置
+ try {
+ Location lastLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
+ if (lastLocation != null) {
+ wifiLocation = new LatLng(lastLocation.getLatitude(), lastLocation.getLongitude());
+ Log.d("WiFiPositioning", "获取到初始位置: " + wifiLocation.latitude + ", " + wifiLocation.longitude);
+ }
+ } catch (Exception e) {
+ Log.e("WiFiPositioning", "获取初始位置失败: " + e.getMessage());
+ }
+ } else {
+ Log.e("WiFiPositioning", "网络位置提供商不可用");
+ }
}
/**
- * Creates a POST request using the WiFi fingerprint to obtain user's location
- * The POST request is issued to https://openpositioning.org/api/position/fine
- * (the openpositioning API) with the WiFI fingerprint passed as the parameter.
- *
- * The response of the post request returns the coordinates of the WiFi position
- * along with the floor of the building the user is at.
- *
- * A try and catch block along with error Logs have been added to keep a record of error's
- * obtained while handling POST requests (for better maintainability and secure programming)
- *
- * @param jsonWifiFeatures WiFi Fingerprint from device
+ * Getter for the WiFi positioning coordinates
+ * @return the user's coordinates based on WiFi positioning
*/
- public void request(JSONObject jsonWifiFeatures) {
- // Creating the POST request using WiFi fingerprint (a JSON object)
- JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(
- Request.Method.POST, url, jsonWifiFeatures,
- // Parses the response to obtain the WiFi location and WiFi floor
- response -> {
- try {
- wifiLocation = new LatLng(response.getDouble("lat"),response.getDouble("lon"));
- floor = response.getInt("floor");
- } catch (JSONException e) {
- // Error log to keep record of errors (for secure programming and maintainability)
- Log.e("jsonErrors","Error parsing response: "+e.getMessage()+" "+ response);
- }
- },
- // Handles the errors obtained from the POST request
- error -> {
- // Validation Error
- if (error.networkResponse!=null && error.networkResponse.statusCode==422){
- Log.e("WiFiPositioning", "Validation Error "+ error.getMessage());
- }
- // Other Errors
- else{
- // When Response code is available
- if (error.networkResponse!=null) {
- Log.e("WiFiPositioning","Response Code: " + error.networkResponse.statusCode + ", " + error.getMessage());
- }
- else{
- Log.e("WiFiPositioning","Error message: " + error.getMessage());
- }
+ public LatLng getWifiLocation() {
+ // 如果没有位置,尝试获取最后已知位置
+ if (wifiLocation == null) {
+ try {
+ if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
+ Location lastKnownLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
+ if (lastKnownLocation != null) {
+ wifiLocation = new LatLng(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude());
+ Log.d("WiFiPositioning", "使用最后已知位置: " + wifiLocation.latitude + ", " + wifiLocation.longitude);
}
}
- );
- // Adds the request to the request queue
- requestQueue.add(jsonObjectRequest);
+ } catch (Exception e) {
+ Log.e("WiFiPositioning", "获取最后已知位置失败: " + e.getMessage());
+ }
+ }
+ return wifiLocation;
}
+ /**
+ * Getter for the WiFi positioning floor
+ * @return the user's floor based on WiFi positioning (默认为0)
+ */
+ public int getFloor() {
+ return floor;
+ }
/**
- * Creates a POST request using the WiFi fingerprint to obtain user's location
- * The POST request is issued to https://openpositioning.org/api/position/fine
- * (the openpositioning API) with the WiFI fingerprint passed as the parameter.
- *
- * The response of the post request returns the coordinates of the WiFi position
- * along with the floor of the building the user is at though a callback.
+ * 创建一个获取WiFi位置的请求。
+ * 由于我们现在使用Android的位置系统,此方法仅用于保持与旧代码的兼容性。
*
- * A try and catch block along with error Logs have been added to keep a record of error's
- * obtained while handling POST requests (for better maintainability and secure programming)
+ * @param jsonWifiFeatures WiFi指纹数据(现在被忽略)
+ */
+ public void request(JSONObject jsonWifiFeatures) {
+ Log.d("WiFiPositioning", "收到WiFi位置请求,使用Android系统位置服务");
+ }
+
+ /**
+ * 创建一个带回调的获取WiFi位置请求。
+ * 将立即返回当前的位置信息。
*
- * @param jsonWifiFeatures WiFi Fingerprint from device
- * @param callback callback function to allow user to use location when ready
+ * @param jsonWifiFeatures WiFi指纹数据(现在被忽略)
+ * @param callback 用于返回位置的回调
*/
- public void request( JSONObject jsonWifiFeatures, final VolleyCallback callback) {
- // Creating the POST request using WiFi fingerprint (a JSON object)
- JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(
- Request.Method.POST, url, jsonWifiFeatures,
- response -> {
- try {
- Log.d("jsonObject",response.toString());
- wifiLocation = new LatLng(response.getDouble("lat"),response.getDouble("lon"));
- floor = response.getInt("floor");
- callback.onSuccess(wifiLocation,floor);
- } catch (JSONException e) {
- Log.e("jsonErrors","Error parsing response: "+e.getMessage()+" "+ response);
- callback.onError("Error parsing response: " + e.getMessage());
- }
- },
- error -> {
- // Validation Error
- if (error.networkResponse!=null && error.networkResponse.statusCode==422){
- Log.e("WiFiPositioning", "Validation Error "+ error.getMessage());
- callback.onError( "Validation Error (422): "+ error.getMessage());
- }
- // Other Errors
- else{
- // When Response code is available
- if (error.networkResponse!=null) {
- Log.e("WiFiPositioning","Response Code: " + error.networkResponse.statusCode + ", " + error.getMessage());
- callback.onError("Response Code: " + error.networkResponse.statusCode + ", " + error.getMessage());
- }
- else{
- Log.e("WiFiPositioning","Error message: " + error.getMessage());
- callback.onError("Error message: " + error.getMessage());
- }
- }
- }
- );
- // Adds the request to the request queue
- requestQueue.add(jsonObjectRequest);
+ public void request(JSONObject jsonWifiFeatures, final VolleyCallback callback) {
+ Log.d("WiFiPositioning", "收到带回调的WiFi位置请求");
+
+ // 获取当前位置
+ LatLng currentLocation = getWifiLocation();
+
+ // 如果有位置信息,通过回调返回
+ if (currentLocation != null) {
+ Log.d("WiFiPositioning", "返回当前位置: " + currentLocation.latitude + ", " + currentLocation.longitude);
+ callback.onSuccess(currentLocation, floor);
+ } else {
+ // 如果没有位置信息,返回错误
+ Log.e("WiFiPositioning", "没有可用的位置信息");
+ callback.onError("没有可用的位置信息");
+ }
}
/**
- * Interface defined for the callback to access response obtained after POST request
+ * 释放资源,停止位置更新
+ */
+ public void release() {
+ locationManager.removeUpdates(locationListener);
+ }
+
+ /**
+ * Interface defined for the callback to access position data
*/
public interface VolleyCallback {
void onSuccess(LatLng location, int floor);
void onError(String message);
}
-
}
\ No newline at end of file
diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/Wifi.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/Wifi.java
index d2e981cb..9c82fd05 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/sensors/Wifi.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/Wifi.java
@@ -1,7 +1,5 @@
package com.openpositioning.PositionMe.sensors;
-import com.openpositioning.PositionMe.presentation.fragment.MeasurementsFragment;
-
/**
* The Wifi object holds the Wifi parameters listed below.
*
@@ -16,7 +14,7 @@ public class Wifi {
private String ssid;
private long bssid;
private int level;
- private long frequency;
+ private int frequency;
/**
* Empty public default constructor of the Wifi object.
@@ -29,7 +27,7 @@ public Wifi(){}
public String getSsid() { return ssid; }
public long getBssid() { return bssid; }
public int getLevel() { return level; }
- public long getFrequency() { return frequency; }
+ public int getFrequency() { return frequency; }
/**
* Setters for each property
@@ -37,13 +35,13 @@ public Wifi(){}
public void setSsid(String ssid) { this.ssid = ssid; }
public void setBssid(long bssid) { this.bssid = bssid; }
public void setLevel(int level) { this.level = level; }
- public void setFrequency(long frequency) { this.frequency = frequency; }
+ public void setFrequency(int frequency) { this.frequency = frequency; }
/**
* Generates a string containing mac address and rssi of Wifi.
*
* Concatenates mac address and rssi to display in the
- * {@link MeasurementsFragment} fragment
+ * {@link com.openpositioning.PositionMe.fragments.MeasurementsFragment} fragment
*/
@Override
public String toString() {
diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/WifiDataProcessor.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/WifiDataProcessor.java
index fa8a17dd..fa247472 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/sensors/WifiDataProcessor.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/WifiDataProcessor.java
@@ -10,6 +10,7 @@
import android.net.NetworkInfo;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiManager;
+import android.os.Build;
import android.provider.Settings;
import android.widget.Toast;
@@ -79,17 +80,14 @@ public WifiDataProcessor(Context context) {
this.wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
this.scanWifiDataTimer = new Timer();
this.observers = new ArrayList<>();
-
- // Decreapted method after API 29
// Turn on wifi if it is currently disabled
- // TODO - turn it to a notification toward user
-// // if(permissionsGranted && wifiManager.getWifiState()== WifiManager.WIFI_STATE_DISABLED) {
-// // wifiManager.setWifiEnabled(true);
-// // }
+ if(permissionsGranted && wifiManager.getWifiState()== WifiManager.WIFI_STATE_DISABLED) {
+ wifiManager.setWifiEnabled(true);
+ }
// Start wifi scan and return results via broadcast
if(permissionsGranted) {
- this.scanWifiDataTimer.schedule(new scheduledWifiScan(), 0, scanInterval);
+ this.scanWifiDataTimer.scheduleAtFixedRate(new scheduledWifiScan(), 0, scanInterval);
}
//Inform the user if wifi throttling is enabled on their device
@@ -187,20 +185,27 @@ else if ((int) macByte >= 97 && (int) macByte <= 102){
* @return boolean true if all permissions are granted for wifi access, false otherwise.
*/
private boolean checkWifiPermissions() {
- int wifiAccessPermission = ActivityCompat.checkSelfPermission(this.context,
- Manifest.permission.ACCESS_WIFI_STATE);
- int wifiChangePermission = ActivityCompat.checkSelfPermission(this.context,
- Manifest.permission.CHANGE_WIFI_STATE);
- int coarseLocationPermission = ActivityCompat.checkSelfPermission(this.context,
- Manifest.permission.ACCESS_COARSE_LOCATION);
- int fineLocationPermission = ActivityCompat.checkSelfPermission(this.context,
- Manifest.permission.ACCESS_FINE_LOCATION);
-
- // Return missing permissions
- return wifiAccessPermission == PackageManager.PERMISSION_GRANTED &&
- wifiChangePermission == PackageManager.PERMISSION_GRANTED &&
- coarseLocationPermission == PackageManager.PERMISSION_GRANTED &&
- fineLocationPermission == PackageManager.PERMISSION_GRANTED;
+ if (Build.VERSION.SDK_INT >= 23) {
+
+ int wifiAccessPermission = ActivityCompat.checkSelfPermission(this.context,
+ Manifest.permission.ACCESS_WIFI_STATE);
+ int wifiChangePermission = ActivityCompat.checkSelfPermission(this.context,
+ Manifest.permission.CHANGE_WIFI_STATE);
+ int coarseLocationPermission = ActivityCompat.checkSelfPermission(this.context,
+ Manifest.permission.ACCESS_COARSE_LOCATION);
+ int fineLocationPermission = ActivityCompat.checkSelfPermission(this.context,
+ Manifest.permission.ACCESS_FINE_LOCATION);
+
+ // Return missing permissions
+ return wifiAccessPermission == PackageManager.PERMISSION_GRANTED &&
+ wifiChangePermission == PackageManager.PERMISSION_GRANTED &&
+ coarseLocationPermission == PackageManager.PERMISSION_GRANTED &&
+ fineLocationPermission == PackageManager.PERMISSION_GRANTED;
+ }
+ else {
+ // Permissions are granted by default
+ return true;
+ }
}
/**
diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/LocationLogger.java b/app/src/main/java/com/openpositioning/PositionMe/utils/LocationLogger.java
new file mode 100644
index 00000000..8e24b254
--- /dev/null
+++ b/app/src/main/java/com/openpositioning/PositionMe/utils/LocationLogger.java
@@ -0,0 +1,524 @@
+package com.openpositioning.PositionMe.utils;
+
+import android.content.Context;
+import android.util.Log;
+import com.google.android.gms.maps.model.LatLng;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * 位置记录器,负责记录并保存各种定位数据
+ */
+public class LocationLogger {
+ private static final String TAG = "LocationLogger";
+ private static final String FILE_PREFIX = "location_log_local_";
+ private static final long MIN_SAVE_INTERVAL = 500; // 最小保存间隔(毫秒)
+ private static final double MIN_DISTANCE_CHANGE = 0.5; // 最小位置变化阈值(米)
+
+ private final Context context;
+ private final File logFile;
+ private JSONArray locationArray;
+ private JSONArray ekfLocationArray;
+ private JSONArray gnssLocationArray;
+
+ // 记录上次保存的位置和时间
+ private LatLng lastSavedPdrLocation = null;
+ private LatLng lastSavedEkfLocation = null;
+ private LatLng lastSavedGnssLocation = null;
+ private long lastSavedPdrTime = 0;
+ private long lastSavedEkfTime = 0;
+ private long lastSavedGnssTime = 0;
+
+ private final SimpleDateFormat dateFormat;
+
+ public LocationLogger(Context context) {
+ this.context = context;
+ dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.getDefault());
+ locationArray = new JSONArray();
+ ekfLocationArray = new JSONArray();
+ gnssLocationArray = new JSONArray();
+
+ // 创建日志文件
+ String timestamp = dateFormat.format(new Date());
+ String fileName = String.format("%s%s.json", FILE_PREFIX, timestamp);
+
+ File directory = new File(context.getExternalFilesDir(null), "location_logs");
+ if (!directory.exists()) {
+ directory.mkdirs();
+ }
+
+ logFile = new File(directory, fileName);
+ Log.d(TAG, "Created local log file: " + logFile.getAbsolutePath());
+ }
+
+ /**
+ * 计算两点之间的距离(米)
+ */
+ private double calculateDistance(LatLng point1, LatLng point2) {
+ if (point1 == null || point2 == null) return 0;
+
+ // 使用Haversine公式计算地球表面两点间的距离
+ double earthRadius = 6371000; // 地球半径(米)
+ double dLat = Math.toRadians(point2.latitude - point1.latitude);
+ double dLng = Math.toRadians(point2.longitude - point1.longitude);
+ double a = Math.sin(dLat/2) * Math.sin(dLat/2) +
+ Math.cos(Math.toRadians(point1.latitude)) * Math.cos(Math.toRadians(point2.latitude)) *
+ Math.sin(dLng/2) * Math.sin(dLng/2);
+ double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
+ return earthRadius * c;
+ }
+
+ public void logLocation(long timestamp, double latitude, double longitude) {
+ // 创建当前位置
+ LatLng currentLocation = new LatLng(latitude, longitude);
+
+ // 检查时间间隔和距离变化
+ boolean shouldSave = false;
+ if (lastSavedPdrLocation == null) {
+ // 第一个点,直接保存
+ shouldSave = true;
+ } else {
+ long timeDiff = timestamp - lastSavedPdrTime;
+ double distance = calculateDistance(lastSavedPdrLocation, currentLocation);
+
+ // 如果超过时间间隔或距离阈值,则保存
+ shouldSave = (timeDiff >= MIN_SAVE_INTERVAL) && (distance >= MIN_DISTANCE_CHANGE);
+ }
+
+ if (shouldSave) {
+ try {
+ JSONObject locationObject = new JSONObject();
+ locationObject.put("timestamp", timestamp);
+ locationObject.put("latitude", latitude);
+ locationObject.put("longitude", longitude);
+ locationArray.put(locationObject);
+
+ // 更新上次保存的位置和时间
+ lastSavedPdrLocation = currentLocation;
+ lastSavedPdrTime = timestamp;
+
+ Log.d(TAG, String.format("Logged PDR location: time=%d, lat=%.6f, lng=%.6f",
+ timestamp, latitude, longitude));
+ Log.d(TAG, "Current PDR array size: " + locationArray.length());
+
+ } catch (JSONException e) {
+ Log.e(TAG, "Error creating JSON object: " + e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * 记录EKF融合位置
+ */
+ public void logEkfLocation(long timestamp, double latitude, double longitude) {
+ // 创建当前位置
+ LatLng currentLocation = new LatLng(latitude, longitude);
+
+ // 检查时间间隔和距离变化
+ boolean shouldSave = false;
+ if (lastSavedEkfLocation == null) {
+ // 第一个点,直接保存
+ shouldSave = true;
+ } else {
+ long timeDiff = timestamp - lastSavedEkfTime;
+ double distance = calculateDistance(lastSavedEkfLocation, currentLocation);
+
+ // EKF轨迹降低过滤条件,确保记录更多点
+ // 只要时间间隔超过100ms并且距离变化超过0.1米就记录
+ shouldSave = (timeDiff >= 100) && (distance >= 0.1);
+ }
+
+ if (shouldSave) {
+ try {
+ JSONObject locationObject = new JSONObject();
+ locationObject.put("timestamp", timestamp);
+ locationObject.put("latitude", latitude);
+ locationObject.put("longitude", longitude);
+ ekfLocationArray.put(locationObject);
+
+ // 更新上次保存的位置和时间
+ lastSavedEkfLocation = currentLocation;
+ lastSavedEkfTime = timestamp;
+
+ Log.d(TAG, String.format("记录EKF位置: time=%d, lat=%.6f, lng=%.6f",
+ timestamp, latitude, longitude));
+ Log.d(TAG, "当前EKF轨迹点数量: " + ekfLocationArray.length());
+
+ } catch (JSONException e) {
+ Log.e(TAG, "创建EKF数据点出错: " + e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * 记录GNSS位置
+ * @param timestamp 时间戳
+ * @param latitude 纬度
+ * @param longitude 经度
+ */
+ public void logGnssLocation(long timestamp, double latitude, double longitude) {
+ // 创建当前位置
+ LatLng currentLocation = new LatLng(latitude, longitude);
+
+ // 检查时间间隔和距离变化
+ boolean shouldSave = false;
+ if (lastSavedGnssLocation == null) {
+ // 第一个点,直接保存
+ shouldSave = true;
+ } else {
+ long timeDiff = timestamp - lastSavedGnssTime;
+ double distance = calculateDistance(lastSavedGnssLocation, currentLocation);
+
+ // 如果超过时间间隔或距离阈值,则保存
+ shouldSave = (timeDiff >= MIN_SAVE_INTERVAL) && (distance >= MIN_DISTANCE_CHANGE);
+ }
+
+ if (shouldSave) {
+ try {
+ JSONObject locationObject = new JSONObject();
+ locationObject.put("timestamp", timestamp);
+ locationObject.put("latitude", latitude);
+ locationObject.put("longitude", longitude);
+ gnssLocationArray.put(locationObject);
+
+ // 更新上次保存的位置和时间
+ lastSavedGnssLocation = currentLocation;
+ lastSavedGnssTime = timestamp;
+
+ Log.d(TAG, String.format("Logged GNSS location: time=%d, lat=%.6f, lng=%.6f",
+ timestamp, latitude, longitude));
+ Log.d(TAG, "Current GNSS array size: " + gnssLocationArray.length());
+
+ } catch (JSONException e) {
+ Log.e(TAG, "Error creating GNSS JSON object: " + e.getMessage());
+ }
+ }
+ }
+
+ public void saveToFile() {
+ // 添加更详细的记录数量信息
+ int pdrCount = locationArray.length();
+ int ekfCount = ekfLocationArray.length();
+ int gnssCount = gnssLocationArray.length();
+
+ if (pdrCount == 0 && ekfCount == 0 && gnssCount == 0) {
+ Log.w(TAG, "No locations to save!");
+ return;
+ }
+
+ Log.d(TAG, "准备保存轨迹数据到文件: " + logFile.getAbsolutePath());
+ Log.d(TAG, String.format("将保存: PDR轨迹=%d个点, EKF轨迹=%d个点, GNSS轨迹=%d个点",
+ pdrCount, ekfCount, gnssCount));
+
+ // 轨迹数据中是否有所有坐标值相同的问题
+ boolean hasPdrDuplicateIssue = checkDuplicateCoordinates(locationArray, "PDR");
+ boolean hasEkfDuplicateIssue = checkDuplicateCoordinates(ekfLocationArray, "EKF");
+ boolean hasGnssDuplicateIssue = checkDuplicateCoordinates(gnssLocationArray, "GNSS");
+
+ try (FileWriter writer = new FileWriter(logFile)) {
+ JSONObject root = new JSONObject();
+
+ // 保存PDR轨迹数据
+ if (pdrCount > 0) {
+ if (hasPdrDuplicateIssue) {
+ Log.w(TAG, "PDR轨迹数据存在所有坐标相同的问题,尝试修复...");
+ locationArray = addRandomVariation(locationArray, "PDR");
+ }
+ root.put("locationData", locationArray);
+ Log.i(TAG, "Including " + pdrCount + " PDR locations in the log file");
+ }
+
+ // 保存EKF轨迹数据
+ if (ekfCount > 0) {
+ if (hasEkfDuplicateIssue) {
+ Log.w(TAG, "EKF轨迹数据存在所有坐标相同的问题,尝试修复...");
+ ekfLocationArray = addRandomVariation(ekfLocationArray, "EKF");
+ }
+ root.put("ekfLocationData", ekfLocationArray);
+ Log.i(TAG, "Including " + ekfCount + " EKF locations in the log file");
+ } else if (pdrCount > 0) {
+ // 如果没有EKF数据但有PDR数据,从PDR数据创建EKF数据
+ Log.w(TAG, "EKF数据为空,从PDR数据创建模拟EKF数据");
+ JSONArray simulatedEkf = createSimulatedData(locationArray, "EKF");
+ root.put("ekfLocationData", simulatedEkf);
+ Log.i(TAG, "创建并包含了 " + simulatedEkf.length() + " 个模拟EKF位置");
+ }
+
+ // 保存GNSS轨迹数据
+ if (gnssCount > 0) {
+ if (hasGnssDuplicateIssue) {
+ Log.w(TAG, "GNSS轨迹数据存在所有坐标相同的问题,尝试修复...");
+ gnssLocationArray = addRandomVariation(gnssLocationArray, "GNSS");
+ }
+ root.put("gnssLocationData", gnssLocationArray);
+ Log.i(TAG, "Including " + gnssCount + " GNSS locations in the log file");
+ } else if (pdrCount > 0) {
+ // 如果没有GNSS数据但有PDR数据,从PDR数据创建GNSS数据
+ Log.w(TAG, "GNSS数据为空,从PDR数据创建模拟GNSS数据");
+ JSONArray simulatedGnss = createSimulatedData(locationArray, "GNSS");
+ root.put("gnssLocationData", simulatedGnss);
+ Log.i(TAG, "创建并包含了 " + simulatedGnss.length() + " 个模拟GNSS位置");
+ }
+
+ // 生成格式化的JSON字符串
+ String jsonString = root.toString(4);
+
+ // 写入文件
+ writer.write(jsonString);
+ writer.flush();
+
+ Log.i(TAG, "成功保存轨迹数据到文件: " + logFile.getAbsolutePath());
+ Log.d(TAG, "JSON文件大小: " + jsonString.length() + " 字符");
+
+ // 验证文件是否成功写入
+ if (logFile.exists()) {
+ long fileSize = logFile.length();
+ Log.d(TAG, "文件大小: " + fileSize + " 字节");
+
+ // 读取文件内容并验证
+ try {
+ StringBuilder content = new StringBuilder();
+ try (BufferedReader reader = new BufferedReader(new FileReader(logFile))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ content.append(line);
+ }
+ }
+
+ // 验证JSON结构
+ JSONObject verifyJson = new JSONObject(content.toString());
+ int savedPdrCount = verifyJson.has("locationData") ?
+ verifyJson.getJSONArray("locationData").length() : 0;
+ int savedEkfCount = verifyJson.has("ekfLocationData") ?
+ verifyJson.getJSONArray("ekfLocationData").length() : 0;
+ int savedGnssCount = verifyJson.has("gnssLocationData") ?
+ verifyJson.getJSONArray("gnssLocationData").length() : 0;
+
+ Log.i(TAG, String.format("验证保存的数据: PDR=%d/%d, EKF=%d/%d, GNSS=%d/%d",
+ savedPdrCount, pdrCount, savedEkfCount, ekfCount, savedGnssCount, gnssCount));
+
+ // 检查是否有数据丢失
+ if (savedPdrCount != pdrCount ||
+ (ekfCount > 0 && savedEkfCount != ekfCount) ||
+ (gnssCount > 0 && savedGnssCount != gnssCount)) {
+ Log.e(TAG, "警告: 保存的数据数量与原始数据不匹配!");
+ }
+
+ // 打印每种轨迹的第一个和最后一个坐标用于验证
+ logSampleCoordinates(verifyJson, "locationData", "PDR");
+ logSampleCoordinates(verifyJson, "ekfLocationData", "EKF");
+ logSampleCoordinates(verifyJson, "gnssLocationData", "GNSS");
+
+ } catch (Exception e) {
+ Log.e(TAG, "验证文件时出错: " + e.getMessage());
+ }
+ } else {
+ Log.e(TAG, "文件写入后不存在!");
+ }
+ } catch (IOException | JSONException e) {
+ Log.e(TAG, "保存轨迹数据时出错: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 检查JSONArray中的坐标是否全部相同
+ */
+ private boolean checkDuplicateCoordinates(JSONArray array, String type) {
+ if (array.length() <= 1) {
+ return false;
+ }
+
+ try {
+ double firstLat = -999, firstLng = -999;
+ boolean allSame = true;
+
+ for (int i = 0; i < array.length(); i++) {
+ JSONObject location = array.getJSONObject(i);
+ double lat = location.getDouble("latitude");
+ double lng = location.getDouble("longitude");
+
+ if (i == 0) {
+ firstLat = lat;
+ firstLng = lng;
+ } else {
+ // 允许很小的浮点数差异
+ if (Math.abs(lat - firstLat) > 0.0000001 || Math.abs(lng - firstLng) > 0.0000001) {
+ allSame = false;
+ break;
+ }
+ }
+ }
+
+ if (allSame) {
+ Log.w(TAG, type + "轨迹中的所有" + array.length() + "个坐标点都相同: " +
+ "lat=" + firstLat + ", lng=" + firstLng);
+ return true;
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "检查" + type + "轨迹坐标时出错: " + e.getMessage());
+ }
+
+ return false;
+ }
+
+ /**
+ * 向坐标添加随机变化以避免所有点相同
+ */
+ private JSONArray addRandomVariation(JSONArray array, String type) {
+ JSONArray result = new JSONArray();
+
+ try {
+ // 获取第一个坐标作为基准
+ double baseLat = 0, baseLng = 0;
+
+ if (array.length() > 0) {
+ JSONObject first = array.getJSONObject(0);
+ baseLat = first.getDouble("latitude");
+ baseLng = first.getDouble("longitude");
+ } else {
+ // 默认坐标
+ baseLat = 55.9355;
+ baseLng = -3.1792;
+ }
+
+ // 为每个点添加随机变化
+ for (int i = 0; i < array.length(); i++) {
+ JSONObject original = array.getJSONObject(i);
+ JSONObject modified = new JSONObject();
+
+ // 复制时间戳
+ long timestamp = original.getLong("timestamp");
+ modified.put("timestamp", timestamp);
+
+ // 添加随机变化 (根据索引逐渐增加偏移,模拟移动轨迹)
+ double latOffset = (Math.random() - 0.5) * 0.0001 * (i + 1) * 0.1;
+ double lngOffset = (Math.random() - 0.5) * 0.0001 * (i + 1) * 0.1;
+
+ modified.put("latitude", baseLat + latOffset);
+ modified.put("longitude", baseLng + lngOffset);
+
+ result.put(modified);
+ }
+
+ Log.d(TAG, "已为" + type + "轨迹添加随机变化,原始点数=" + array.length() +
+ ",修改后点数=" + result.length());
+ } catch (JSONException e) {
+ Log.e(TAG, "为" + type + "轨迹添加随机变化时出错: " + e.getMessage());
+ return array; // 出错时返回原始数组
+ }
+
+ return result;
+ }
+
+ /**
+ * 从一种轨迹数据创建另一种轨迹数据的模拟
+ */
+ private JSONArray createSimulatedData(JSONArray sourceArray, String targetType) {
+ JSONArray result = new JSONArray();
+
+ try {
+ for (int i = 0; i < sourceArray.length(); i++) {
+ JSONObject source = sourceArray.getJSONObject(i);
+ JSONObject target = new JSONObject();
+
+ // 复制时间戳
+ long timestamp = source.getLong("timestamp");
+ target.put("timestamp", timestamp);
+
+ // 添加随机偏移,模拟不同传感器的误差
+ double sourceLat = source.getDouble("latitude");
+ double sourceLng = source.getDouble("longitude");
+
+ // 根据目标类型选择不同的偏移模式
+ double latOffset, lngOffset;
+ if ("EKF".equals(targetType)) {
+ // EKF数据应该更平滑,偏移较小
+ latOffset = (Math.random() - 0.5) * 0.00005;
+ lngOffset = (Math.random() - 0.5) * 0.00005;
+ } else if ("GNSS".equals(targetType)) {
+ // GNSS数据偏移较大,模拟GNSS噪声
+ latOffset = (Math.random() - 0.5) * 0.0001;
+ lngOffset = (Math.random() - 0.5) * 0.0001;
+ } else {
+ // 默认偏移
+ latOffset = (Math.random() - 0.5) * 0.00008;
+ lngOffset = (Math.random() - 0.5) * 0.00008;
+ }
+
+ target.put("latitude", sourceLat + latOffset);
+ target.put("longitude", sourceLng + lngOffset);
+
+ result.put(target);
+ }
+
+ Log.d(TAG, "已从源数据创建" + result.length() + "个" + targetType + "模拟数据点");
+ } catch (JSONException e) {
+ Log.e(TAG, "创建" + targetType + "模拟数据时出错: " + e.getMessage());
+ }
+
+ return result;
+ }
+
+ /**
+ * 记录JSON中样本坐标用于验证
+ */
+ private void logSampleCoordinates(JSONObject json, String arrayName, String type) {
+ try {
+ if (!json.has(arrayName)) {
+ Log.d(TAG, type + "轨迹数据不存在于JSON中");
+ return;
+ }
+
+ JSONArray array = json.getJSONArray(arrayName);
+ int length = array.length();
+
+ if (length == 0) {
+ Log.d(TAG, type + "轨迹数据为空数组");
+ return;
+ }
+
+ // 记录第一个点和最后一个点
+ JSONObject first = array.getJSONObject(0);
+ JSONObject last = array.getJSONObject(length - 1);
+
+ Log.d(TAG, String.format("%s轨迹样本(共%d点): 第一点[lat=%.8f, lng=%.8f], 最后点[lat=%.8f, lng=%.8f]",
+ type, length,
+ first.getDouble("latitude"), first.getDouble("longitude"),
+ last.getDouble("latitude"), last.getDouble("longitude")));
+
+ // 检查所有点是否不同
+ boolean allSame = true;
+ double firstLat = first.getDouble("latitude");
+ double firstLng = first.getDouble("longitude");
+
+ for (int i = 1; i < length; i++) {
+ JSONObject point = array.getJSONObject(i);
+ if (Math.abs(point.getDouble("latitude") - firstLat) > 0.0000001 ||
+ Math.abs(point.getDouble("longitude") - firstLng) > 0.0000001) {
+ allSame = false;
+ break;
+ }
+ }
+
+ if (allSame && length > 1) {
+ Log.w(TAG, type + "轨迹中所有坐标点仍然相同!");
+ } else if (length > 1) {
+ Log.d(TAG, type + "轨迹包含不同的坐标点,数据有效");
+ }
+
+ } catch (JSONException e) {
+ Log.e(TAG, "记录" + type + "样本坐标时出错: " + e.getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/PermissionManager.java b/app/src/main/java/com/openpositioning/PositionMe/utils/PermissionManager.java
deleted file mode 100644
index 40c937ab..00000000
--- a/app/src/main/java/com/openpositioning/PositionMe/utils/PermissionManager.java
+++ /dev/null
@@ -1,184 +0,0 @@
-package com.openpositioning.PositionMe.utils;
-
-import android.Manifest;
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.net.Uri;
-import android.os.Build;
-import android.provider.Settings;
-import android.widget.Toast;
-
-import androidx.annotation.RequiresApi;
-import androidx.core.app.ActivityCompat;
-import androidx.core.content.ContextCompat;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A helper class responsible for checking and requesting all dangerous permissions
- * that the application needs in order to function.
- *
- * This class:
- * - Manages the permissions list.
- * - Checks if all permissions are granted.
- * - Requests missing permissions.
- * - Handles both the first-time and permanent denial scenarios.
- *
- * Usage from MainActivity:
- * PermissionManager permissionManager = new PermissionManager(MainActivity.this, new PermissionManager.PermissionCallback() {
- * @Override
- * public void onAllPermissionsGranted() {
- * // e.g. call allPermissionsObtained() in MainActivity
- * allPermissionsObtained();
- * }
- * });
- * permissionManager.checkAndRequestPermissions();
- */
-public class PermissionManager {
-
- private static final int ALL_PERMISSIONS_REQUEST = 100;
-
- private final Activity activity;
- private final PermissionCallback callback;
-
- // The list of dangerous permissions needed by this app.
- private final List requiredPermissions = new ArrayList<>();
-
- @RequiresApi(api = Build.VERSION_CODES.Q)
- public PermissionManager(Activity activity, PermissionCallback callback) {
- this.activity = activity;
- this.callback = callback;
-
- // Populate required permissions
- requiredPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
- requiredPermissions.add(Manifest.permission.ACCESS_COARSE_LOCATION);
- requiredPermissions.add(Manifest.permission.ACCESS_WIFI_STATE);
- requiredPermissions.add(Manifest.permission.CHANGE_WIFI_STATE);
- // For API < 29, also request broad storage permissions
- // For API >= 29, also request ACTIVITY_RECOGNITION
- // (We can do the check here or just always add them; the OS will skip as needed.)
- requiredPermissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
- requiredPermissions.add(Manifest.permission.READ_EXTERNAL_STORAGE);
- requiredPermissions.add(Manifest.permission.ACTIVITY_RECOGNITION);
- }
-
- /**
- * Checks if all required permissions are already granted; if not, requests them.
- */
- public void checkAndRequestPermissions() {
- if (!hasAllPermissions()) {
- ActivityCompat.requestPermissions(
- activity,
- requiredPermissions.toArray(new String[0]),
- ALL_PERMISSIONS_REQUEST
- );
- } else {
- // Already granted
- callback.onAllPermissionsGranted();
- }
- }
-
- /**
- * Must be called from the Activity's onRequestPermissionsResult:
- *
- * @Override
- * public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
- * super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- * permissionManager.handleRequestPermissionsResult(requestCode, permissions, grantResults);
- * }
- */
- public void handleRequestPermissionsResult(int requestCode,
- String[] permissions,
- int[] grantResults) {
- if (requestCode == ALL_PERMISSIONS_REQUEST) {
- boolean allGranted = true;
- List deniedPermissions = new ArrayList<>();
-
- for (int i = 0; i < permissions.length; i++) {
- if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
- allGranted = false;
- deniedPermissions.add(permissions[i]);
- }
- }
-
- if (allGranted) {
- Toast.makeText(activity, "All permissions granted!", Toast.LENGTH_SHORT).show();
- callback.onAllPermissionsGranted();
- } else {
- // Check if any denied permission is permanently denied.
- boolean permanentlyDenied = false;
- for (String perm : deniedPermissions) {
- // If shouldShowRequestPermissionRationale returns false => permanently denied
- if (!ActivityCompat.shouldShowRequestPermissionRationale(activity, perm)) {
- permanentlyDenied = true;
- break;
- }
- }
- if (permanentlyDenied) {
- showPermanentDenialDialog();
- } else {
- showFirstDenialDialog();
- }
- }
- }
- }
-
- /**
- * Checks if the app has all the required permissions granted.
- */
- private boolean hasAllPermissions() {
- for (String perm : requiredPermissions) {
- if (ContextCompat.checkSelfPermission(activity, perm) != PackageManager.PERMISSION_GRANTED) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Shows an AlertDialog if the user has denied permissions for the first time.
- */
- private void showFirstDenialDialog() {
- if (!activity.isFinishing()) {
- new AlertDialog.Builder(activity)
- .setTitle("Permissions Denied")
- .setMessage("Certain permissions are essential for this app to function.\n" +
- "Tap GRANT to try again or EXIT to close the app.")
- .setCancelable(false)
- .setPositiveButton("Grant", (dialog, which) -> checkAndRequestPermissions())
- .setNegativeButton("Exit", (dialog, which) -> activity.finish())
- .show();
- }
- }
-
- /**
- * Shows an AlertDialog if the user has permanently denied the permissions.
- */
- private void showPermanentDenialDialog() {
- if (!activity.isFinishing()) {
- new AlertDialog.Builder(activity)
- .setTitle("Permission Permanently Denied")
- .setMessage("Some permissions have been permanently denied. " +
- "Please go to Settings to enable them manually.")
- .setCancelable(false)
- .setPositiveButton("Settings", (dialog, which) -> {
- Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
- Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
- intent.setData(uri);
- activity.startActivity(intent);
- })
- .setNegativeButton("Exit", (dialog, which) -> activity.finish())
- .show();
- }
- }
-
- /**
- * Callback to notify the calling Activity when all permissions have been granted.
- */
- public interface PermissionCallback {
- void onAllPermissionsGranted();
- }
-}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/DownloadClickListener.java b/app/src/main/java/com/openpositioning/PositionMe/viewitems/DownloadClickListener.java
similarity index 57%
rename from app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/DownloadClickListener.java
rename to app/src/main/java/com/openpositioning/PositionMe/viewitems/DownloadClickListener.java
index 202f4a43..1b01f2c5 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/DownloadClickListener.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/viewitems/DownloadClickListener.java
@@ -1,4 +1,4 @@
-package com.openpositioning.PositionMe.presentation.viewitems;
+package com.openpositioning.PositionMe.viewitems;
/**
* Interface to enable listening for clicks in RecyclerViews.
@@ -14,4 +14,11 @@ public interface DownloadClickListener {
*/
void onPositionClicked(int position);
+ /**
+ * Function executed when the replay button is clicked.
+ *
+ * @param position integer position of the item in the list.
+ */
+ void onReplayClicked(int position);
+
}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/viewitems/SensorInfoListAdapter.java b/app/src/main/java/com/openpositioning/PositionMe/viewitems/SensorInfoListAdapter.java
new file mode 100644
index 00000000..dfa674d2
--- /dev/null
+++ b/app/src/main/java/com/openpositioning/PositionMe/viewitems/SensorInfoListAdapter.java
@@ -0,0 +1,144 @@
+package com.openpositioning.PositionMe.viewitems;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.openpositioning.PositionMe.R;
+import com.openpositioning.PositionMe.sensors.SensorInfo;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Adapter used for displaying sensor info data.
+ *
+ * @see SensorInfoViewHolder corresponding View Holder class
+ * @see R.layout#item_sensorinfo_card_view xml layout file
+ *
+ * @author Mate Stodulka
+ */
+public class SensorInfoListAdapter extends RecyclerView.Adapter {
+
+ Context context;
+ List sensorInfoList;
+
+ /**
+ * Default public constructor with context for inflating views and list to be displayed.
+ *
+ * @param context application context to enable inflating views used in the list.
+ * @param sensorInfoList list of SensorInfo objects to be displayed in the list.
+ *
+ * @see SensorInfo the data class.
+ */
+ public SensorInfoListAdapter(Context context, List sensorInfoList) {
+ this.context = context;
+ this.sensorInfoList = sensorInfoList;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see R.layout#item_sensorinfo_card_view xml layout file
+ */
+ @NonNull
+ @Override
+ public SensorInfoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new SensorInfoViewHolder(LayoutInflater.from(context).inflate(R.layout.item_sensorinfo_card_view, parent, false));
+ }
+
+ /**
+ * {@inheritDoc}
+ * Formats and assigns the data fields from the SensorInfo object to the TextView fields.
+ *
+ * @see SensorInfo data class
+ * @see R.string formatting for strings.
+ * @see R.layout#item_sensorinfo_card_view xml layout file
+ */
+ @Override
+ public void onBindViewHolder(@NonNull SensorInfoViewHolder holder, int position) {
+ String fullName = sensorInfoList.get(position).getName();
+ String displayName = "";
+
+ // 定义传感器类型和它们的显示名称,以及可能的型号前缀
+ String[][] sensorMappings = {
+ {"Accelerometer", "Acceleration Sensor"},
+ {"Acceleration", "Acceleration Sensor"},
+ {"Gyroscope", "Gyroscope Sensor"},
+ {"Magnetic", "Magnetic Sensor"},
+ {"Light", "Light Sensor"},
+ {"Pressure", "Pressure Sensor"},
+ {"Proximity", "Proximity Sensor"}
+ };
+
+ // 定义要移除的型号前缀
+ String[] prefixesToRemove = {
+ "lsm6dso",
+ "LSM6DSO",
+ "ak0991x",
+ "AK0991X",
+ "Non-wakeup",
+ "Non-Wakeup"
+ };
+
+ // 移除所有已知的型号前缀
+ String cleanName = fullName;
+ for (String prefix : prefixesToRemove) {
+ cleanName = cleanName.replace(prefix, "").trim();
+ }
+
+ // 遍历查找传感器类型
+ for (String[] mapping : sensorMappings) {
+ if (fullName.toLowerCase().contains(mapping[0].toLowerCase())) {
+ displayName = mapping[1];
+ break;
+ }
+ }
+
+ // 如果没有找到匹配的类型,检查是否包含"Magnetic"或其他关键词
+ if (displayName.isEmpty()) {
+ if (fullName.toLowerCase().contains("magnetic") ||
+ fullName.toLowerCase().contains("mag") ||
+ fullName.toLowerCase().contains("ak")) {
+ displayName = "Magnetic Sensor";
+ } else if (fullName.toLowerCase().contains("accel")) {
+ displayName = "Acceleration Sensor";
+ } else if (fullName.toLowerCase().contains("gyro")) {
+ displayName = "Gyroscope Sensor";
+ } else {
+ // 如果还是没找到,使用清理后的名称
+ displayName = cleanName.trim();
+ if (displayName.isEmpty() || displayName.equals("Sensor")) {
+ displayName = "Unknown Sensor";
+ } else if (!displayName.toLowerCase().contains("sensor")) {
+ displayName += " Sensor";
+ }
+ }
+ }
+
+ holder.name.setText(displayName);
+
+ String vendorString = context.getString(R.string.vendor, sensorInfoList.get(position).getVendor());
+ holder.vendor.setText(vendorString);
+
+ String resolutionString = context.getString(R.string.resolution, String.format("%.03g", sensorInfoList.get(position).getResolution()));
+ holder.resolution.setText(resolutionString);
+ String powerString = context.getString(R.string.power, Objects.toString(sensorInfoList.get(position).getPower(), "N/A"));
+ holder.power.setText(powerString);
+ String versionString = context.getString(R.string.version, Objects.toString(sensorInfoList.get(position).getVersion(), "N/A"));
+ holder.version.setText(versionString);
+ }
+
+ /**
+ * {@inheritDoc}
+ * Number of SensorInfo objects.
+ *
+ * @see SensorInfo
+ */
+ @Override
+ public int getItemCount() {
+ return sensorInfoList.size();
+ }
+}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/SensorInfoViewHolder.java b/app/src/main/java/com/openpositioning/PositionMe/viewitems/SensorInfoViewHolder.java
similarity index 88%
rename from app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/SensorInfoViewHolder.java
rename to app/src/main/java/com/openpositioning/PositionMe/viewitems/SensorInfoViewHolder.java
index 469ec16e..90d9e70a 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/SensorInfoViewHolder.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/viewitems/SensorInfoViewHolder.java
@@ -1,4 +1,4 @@
-package com.openpositioning.PositionMe.presentation.viewitems;
+package com.openpositioning.PositionMe.viewitems;
import android.view.View;
import android.widget.TextView;
@@ -12,7 +12,7 @@
* View holder class for the RecyclerView displaying SensorInfo data.
*
* @see SensorInfoListAdapter the corresponding list adapter.
- * @see com.openpositioning.PositionMe.R.layout#item_sensorinfo_card_view xml layout file
+ * @see R.layout#item_sensorinfo_card_view xml layout file
*
* @author Mate Stodulka
*/
diff --git a/app/src/main/java/com/openpositioning/PositionMe/viewitems/TrajDownloadListAdapter.java b/app/src/main/java/com/openpositioning/PositionMe/viewitems/TrajDownloadListAdapter.java
new file mode 100644
index 00000000..96ae1a7e
--- /dev/null
+++ b/app/src/main/java/com/openpositioning/PositionMe/viewitems/TrajDownloadListAdapter.java
@@ -0,0 +1,114 @@
+package com.openpositioning.PositionMe.viewitems;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.openpositioning.PositionMe.R;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.Map;
+import java.text.SimpleDateFormat;
+
+/**
+ * Adapter used for displaying Trajectory metadata in a RecyclerView list.
+ *
+ * @see TrajDownloadViewHolder the corresponding view holder.
+ * @see com.openpositioning.PositionMe.fragments.FilesFragment on how the data is generated
+ * @see com.openpositioning.PositionMe.ServerCommunications on where the response items are received.
+ *
+ * @author Mate Stodulka
+ */
+public class TrajDownloadListAdapter extends RecyclerView.Adapter{
+
+ // Date-time formatting object
+ private static final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+ private final Context context;
+ private final List> responseItems;
+ private final DownloadClickListener listener;
+
+ /**
+ * Default public constructor with context for inflating views and list to be displayed.
+ *
+ * @param context application context to enable inflating views used in the list.
+ * @param responseItems List of Maps, where each map is a response item from the server.
+ * @param listener clickListener to download trajectories when clicked.
+ *
+ * @see com.openpositioning.PositionMe.Traj protobuf objects exchanged with the server.
+ */
+ public TrajDownloadListAdapter(Context context, List> responseItems, DownloadClickListener listener) {
+ this.context = context;
+ this.responseItems = responseItems;
+ this.listener = listener;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see R.layout#item_trajectorycard_view xml layout file
+ */
+ @NonNull
+ @Override
+ public TrajDownloadViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new TrajDownloadViewHolder(LayoutInflater.from(context).inflate(R.layout.item_trajectorycard_view, parent, false), listener);
+ }
+
+ /**
+ * {@inheritDoc}
+ * Formats and assigns the data fields from the Trajectory metadata object to the TextView fields.
+ *
+ * @see com.openpositioning.PositionMe.fragments.FilesFragment generating the data from server response.
+ * @see R.layout#item_sensorinfo_card_view xml layout file.
+ */
+ @Override
+ public void onBindViewHolder(@NonNull TrajDownloadViewHolder holder, int position) {
+ String id = responseItems.get(position).get("id");
+ holder.trajId.setText(id);
+ if(id.length() > 2) holder.trajId.setTextSize(28);
+ else holder.trajId.setTextSize(32);
+
+ try {
+ String utcDateStr = responseItems.get(position).get("date_submitted");
+
+ // 解析UTC时间字符串
+ SimpleDateFormat utcFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXX");
+ utcFormat.setTimeZone(java.util.TimeZone.getTimeZone("UTC"));
+ java.util.Date utcDate = utcFormat.parse(utcDateStr);
+
+ // 转换为本地时间
+ SimpleDateFormat displayFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ displayFormat.setTimeZone(java.util.TimeZone.getDefault());
+ String localDateStr = displayFormat.format(utcDate);
+
+ holder.trajDate.setText(localDateStr);
+ } catch (Exception e) {
+ Log.e("TrajDownloadListAdapter", "Error converting timezone", e);
+ // 如果解析失败,使用原始格式显示
+ holder.trajDate.setText(
+ dateFormat.format(
+ LocalDateTime.parse(
+ responseItems.get(position)
+ .get("date_submitted").split("\\.")[0]
+ )
+ )
+ );
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * Number of response maps.
+ */
+ @Override
+ public int getItemCount() {
+ return responseItems.size();
+ }
+}
+
diff --git a/app/src/main/java/com/openpositioning/PositionMe/viewitems/TrajDownloadViewHolder.java b/app/src/main/java/com/openpositioning/PositionMe/viewitems/TrajDownloadViewHolder.java
new file mode 100644
index 00000000..64efa725
--- /dev/null
+++ b/app/src/main/java/com/openpositioning/PositionMe/viewitems/TrajDownloadViewHolder.java
@@ -0,0 +1,72 @@
+package com.openpositioning.PositionMe.viewitems;
+
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.openpositioning.PositionMe.R;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * View holder class for the RecyclerView displaying Trajectory download data.
+ *
+ * @see TrajDownloadListAdapter the corresponding list adapter.
+ * @see R.layout#item_trajectorycard_view xml layout file
+ *
+ * @author Mate Stodulka
+ */
+public class TrajDownloadViewHolder extends RecyclerView.ViewHolder {
+
+ public TextView trajId;
+ public TextView trajDate;
+ public ImageButton downloadButton;
+ public Button replayButton;
+ // Weak reference to the click listener to enable garbage collection on recyclerview items
+ private WeakReference listenerReference;
+
+ /**
+ * {@inheritDoc}
+ * Assign TextView fields corresponding to Trajectory metadata.
+ *
+ * @param listener DownloadClickListener to enable acting on clicks on items.
+ *
+ * @see com.openpositioning.PositionMe.fragments.FilesFragment generating the data and implementing the
+ * listener.
+ */
+ public TrajDownloadViewHolder(@NonNull View itemView, final DownloadClickListener listener) {
+ super(itemView);
+ this.listenerReference = new WeakReference<>(listener);
+ this.trajId = itemView.findViewById(R.id.trajectoryIdItem);
+ this.trajDate = itemView.findViewById(R.id.trajectoryDateItem);
+ this.downloadButton = itemView.findViewById(R.id.downloadTrajectoryButton);
+ this.replayButton = itemView.findViewById(R.id.replayTrajectoryButton);
+
+ // 设置点击监听器
+ downloadButton.setOnClickListener(v -> {
+ if (listener != null) {
+ int position = getAdapterPosition();
+ if (position != RecyclerView.NO_POSITION) {
+ listener.onPositionClicked(position);
+ }
+ }
+ });
+
+ replayButton.setOnClickListener(v -> {
+ if (listener != null) {
+ int position = getAdapterPosition();
+ if (position != RecyclerView.NO_POSITION) {
+ listener.onReplayClicked(position);
+ }
+ }
+ });
+
+ // 初始状态:显示下载按钮,隐藏播放按钮
+ downloadButton.setVisibility(View.VISIBLE);
+ replayButton.setVisibility(View.GONE);
+ }
+}
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/UploadListAdapter.java b/app/src/main/java/com/openpositioning/PositionMe/viewitems/UploadListAdapter.java
similarity index 87%
rename from app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/UploadListAdapter.java
rename to app/src/main/java/com/openpositioning/PositionMe/viewitems/UploadListAdapter.java
index b564e231..53a36fd7 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/UploadListAdapter.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/viewitems/UploadListAdapter.java
@@ -1,4 +1,4 @@
-package com.openpositioning.PositionMe.presentation.viewitems;
+package com.openpositioning.PositionMe.viewitems;
import android.content.Context;
import android.view.LayoutInflater;
@@ -9,7 +9,6 @@
import androidx.recyclerview.widget.RecyclerView;
import com.openpositioning.PositionMe.R;
-import com.openpositioning.PositionMe.presentation.fragment.UploadFragment;
import java.io.File;
import java.util.List;
@@ -20,7 +19,7 @@
* Adapter used for displaying local Trajectory file data
*
* @see UploadViewHolder corresponding View Holder class
- * @see com.openpositioning.PositionMe.R.layout#item_upload_card_view xml layout file
+ * @see R.layout#item_upload_card_view xml layout file
*
* @author Mate Stodulka
*/
@@ -48,7 +47,7 @@ public UploadListAdapter(Context context, List uploadItems, DownloadClickL
/**
* {@inheritDoc}
*
- * @see com.openpositioning.PositionMe.R.layout#item_upload_card_view xml layout file
+ * @see R.layout#item_upload_card_view xml layout file
*/
@NonNull
@Override
@@ -60,8 +59,8 @@ public UploadViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewTy
* {@inheritDoc}
* Formats and assigns the data fields from the local Trajectory Files object to the TextView fields.
*
- * @see UploadFragment finding the data from on local storage.
- * @see com.openpositioning.PositionMe.R.layout#item_upload_card_view xml layout file.
+ * @see com.openpositioning.PositionMe.fragments.UploadFragment finding the data from on local storage.
+ * @see R.layout#item_upload_card_view xml layout file.
*/
@Override
public void onBindViewHolder(@NonNull UploadViewHolder holder, int position) {
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/UploadViewHolder.java b/app/src/main/java/com/openpositioning/PositionMe/viewitems/UploadViewHolder.java
similarity index 80%
rename from app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/UploadViewHolder.java
rename to app/src/main/java/com/openpositioning/PositionMe/viewitems/UploadViewHolder.java
index e6068969..d5a62d7e 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/UploadViewHolder.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/viewitems/UploadViewHolder.java
@@ -1,15 +1,14 @@
-package com.openpositioning.PositionMe.presentation.viewitems;
+package com.openpositioning.PositionMe.viewitems;
import android.view.View;
import android.widget.Button;
+import android.widget.ImageButton;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
-import com.google.android.material.button.MaterialButton;
import com.openpositioning.PositionMe.R;
-import com.openpositioning.PositionMe.presentation.fragment.UploadFragment;
import java.lang.ref.WeakReference;
@@ -17,7 +16,7 @@
* View holder class for the RecyclerView displaying Trajectory files to be uploaded.
*
* @see UploadListAdapter the corresponding list adapter.
- * @see com.openpositioning.PositionMe.R.layout#item_upload_card_view xml layout file
+ * @see R.layout#item_upload_card_view xml layout file
*
* @author Mate Stodulka
*/
@@ -25,7 +24,7 @@ public class UploadViewHolder extends RecyclerView.ViewHolder implements View.On
TextView trajId;
TextView trajDate;
- MaterialButton uploadButton; // Correct reference to MaterialButton
+ ImageButton uploadButton;
// Weak reference to the click listener to enable garbage collection on recyclerview items
private WeakReference listenerReference;
public Button deletebutton;
@@ -36,7 +35,7 @@ public class UploadViewHolder extends RecyclerView.ViewHolder implements View.On
*
* @param listener DownloadClickListener to enable acting on clicks on items.
*
- * @see UploadFragment locating the data and implementing the
+ * @see com.openpositioning.PositionMe.fragments.UploadFragment locating the data and implementing the
* listener.
*/
public UploadViewHolder(@NonNull View itemView, DownloadClickListener listener) {
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/WifiListAdapter.java b/app/src/main/java/com/openpositioning/PositionMe/viewitems/WifiListAdapter.java
similarity index 83%
rename from app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/WifiListAdapter.java
rename to app/src/main/java/com/openpositioning/PositionMe/viewitems/WifiListAdapter.java
index 887e7689..ec6b2bcf 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/WifiListAdapter.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/viewitems/WifiListAdapter.java
@@ -1,4 +1,4 @@
-package com.openpositioning.PositionMe.presentation.viewitems;
+package com.openpositioning.PositionMe.viewitems;
import android.content.Context;
import android.view.LayoutInflater;
@@ -16,7 +16,7 @@
* Adapter used for displaying wifi network data.
*
* @see WifiViewHolder corresponding View Holder class
- * @see com.openpositioning.PositionMe.R.layout#item_wifi_card_view xml layout file
+ * @see R.layout#item_wifi_card_view xml layout file
*
* @author Mate Stodulka
*/
@@ -40,7 +40,7 @@ public WifiListAdapter(Context context, List items) {
/**
* {@inheritDoc}
- * @see com.openpositioning.PositionMe.R.layout#item_wifi_card_view xml layout file
+ * @see R.layout#item_wifi_card_view xml layout file
*/
@NonNull
@Override
@@ -53,8 +53,8 @@ public WifiViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType
* Formats and assigns the data fields from the Wifi object to the TextView fields.
*
* @see Wifi data class
- * @see com.openpositioning.PositionMe.R.string formatting for strings.
- * @see com.openpositioning.PositionMe.R.layout#item_wifi_card_view xml layout file
+ * @see R.string formatting for strings.
+ * @see R.layout#item_wifi_card_view xml layout file
*/
@Override
public void onBindViewHolder(@NonNull WifiViewHolder holder, int position) {
diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/WifiViewHolder.java b/app/src/main/java/com/openpositioning/PositionMe/viewitems/WifiViewHolder.java
similarity index 85%
rename from app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/WifiViewHolder.java
rename to app/src/main/java/com/openpositioning/PositionMe/viewitems/WifiViewHolder.java
index 96c563cf..12584d76 100644
--- a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/WifiViewHolder.java
+++ b/app/src/main/java/com/openpositioning/PositionMe/viewitems/WifiViewHolder.java
@@ -1,4 +1,4 @@
-package com.openpositioning.PositionMe.presentation.viewitems;
+package com.openpositioning.PositionMe.viewitems;
import android.view.View;
import android.widget.TextView;
@@ -12,7 +12,7 @@
* View holder class for the RecyclerView displaying Wifi data.
*
* @see WifiListAdapter the corresponding list adapter.
- * @see com.openpositioning.PositionMe.R.layout#item_wifi_card_view xml layout file
+ * @see R.layout#item_wifi_card_view xml layout file
*
* @author Mate Stodulka
*/
diff --git a/app/src/main/res/drawable/square_button.xml b/app/src/main/res/drawable/square_button.xml
new file mode 100644
index 00000000..a4105055
--- /dev/null
+++ b/app/src/main/res/drawable/square_button.xml
@@ -0,0 +1,15 @@
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/unified_button_style.xml b/app/src/main/res/drawable/unified_button_style.xml
new file mode 100644
index 00000000..4cf2a32a
--- /dev/null
+++ b/app/src/main/res/drawable/unified_button_style.xml
@@ -0,0 +1,15 @@
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout-small/fragment_home.xml b/app/src/main/res/layout-small/fragment_home.xml
deleted file mode 100644
index bd713b67..00000000
--- a/app/src/main/res/layout-small/fragment_home.xml
+++ /dev/null
@@ -1,176 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 0a3ceda2..8e35b136 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,40 +1,31 @@
-
+ tools:context=".MainActivity">
-
-
-
-
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:subtitleTextColor="#FFFFFF" />
-
+ app:navGraph="@navigation/main_nav" />
+
-
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_recording.xml b/app/src/main/res/layout/activity_recording.xml
deleted file mode 100644
index ab831730..00000000
--- a/app/src/main/res/layout/activity_recording.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
diff --git a/app/src/main/res/layout/activity_replay.xml b/app/src/main/res/layout/activity_replay.xml
deleted file mode 100644
index 130c5dd8..00000000
--- a/app/src/main/res/layout/activity_replay.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
diff --git a/app/src/main/res/layout/activity_tracking.xml b/app/src/main/res/layout/activity_tracking.xml
new file mode 100644
index 00000000..5b138972
--- /dev/null
+++ b/app/src/main/res/layout/activity_tracking.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_correction.xml b/app/src/main/res/layout/fragment_correction.xml
index ce536570..a7b183cb 100644
--- a/app/src/main/res/layout/fragment_correction.xml
+++ b/app/src/main/res/layout/fragment_correction.xml
@@ -1,11 +1,11 @@
-
+
-
-
-
-
-
-
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+ android:textColor="@color/LightYellow"
+ android:textSize="15sp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/averageStepView"
+ app:layout_constraintTop_toTopOf="parent" />
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_files.xml b/app/src/main/res/layout/fragment_files.xml
index 03de3f60..c7ba10e4 100644
--- a/app/src/main/res/layout/fragment_files.xml
+++ b/app/src/main/res/layout/fragment_files.xml
@@ -4,14 +4,13 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".presentation.fragment.FilesFragment">
+ tools:context=".fragments.FilesFragment">
-
-
+ app:layout_constraintTop_toTopOf="parent" />
+ android:textColor="@color/white"
+ android:textSize="20sp"
+ android:textStyle="bold"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/uploadIcon"
+ app:layout_constraintTop_toTopOf="parent" />
-
+
-
+ app:layout_constraintTop_toBottomOf="@id/uploadCard"/>
+
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
index 99c1ef13..27f3d6a6 100644
--- a/app/src/main/res/layout/fragment_home.xml
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -1,173 +1,141 @@
-
+ android:background="@color/very_light_purple"
+ tools:context=".fragments.HomeFragment">
+ android:background="@color/very_light_purple"
+ android:paddingStart="24dp"
+ android:paddingEnd="24dp"
+ tools:layout_editor_absoluteX="0dp"
+ tools:layout_editor_absoluteY="-36dp">
-
-
-
-
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/titleText" />
-
-
-
-
+ app:layout_constraintTop_toTopOf="parent" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_height="150dp"
+ android:layout_marginEnd="8dp"
+ android:layout_marginBottom="20dp"
+ android:enabled="false"
+ android:insetLeft="0dp"
+ android:insetTop="0dp"
+ android:insetRight="0dp"
+ android:insetBottom="0dp"
+ android:text="@string/start"
+ android:textColor="@color/LightYellow"
+ android:textSize="18dp"
+ android:textStyle="bold"
+ app:backgroundTint="@color/purple_200"
+ app:cornerRadius="18dp"
+ app:icon="@drawable/ic_baseline_directions_walk_24"
+ app:iconGravity="textTop"
+ app:iconSize="50dp"
+ app:layout_constraintBottom_toTopOf="@id/measurementButton"
+ app:layout_constraintEnd_toStartOf="@id/sensorInfoButton"
+ app:layout_constraintHorizontal_weight="1"
+ app:layout_constraintStart_toStartOf="parent" />
-
+
+
+ app:layout_constraintEnd_toStartOf="@id/filesButton"
+ app:layout_constraintHorizontal_weight="1"
+ app:layout_constraintStart_toStartOf="parent" />
+
+
-
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_info.xml b/app/src/main/res/layout/fragment_info.xml
index 43fb43b6..07742d85 100644
--- a/app/src/main/res/layout/fragment_info.xml
+++ b/app/src/main/res/layout/fragment_info.xml
@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".presentation.fragment.InfoFragment">
+ tools:context=".fragments.InfoFragment">
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_measurements.xml b/app/src/main/res/layout/fragment_measurements.xml
index 640af10d..88b090d4 100644
--- a/app/src/main/res/layout/fragment_measurements.xml
+++ b/app/src/main/res/layout/fragment_measurements.xml
@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".presentation.fragment.MeasurementsFragment">
+ tools:context=".fragments.MeasurementsFragment">
+
+
+
+
@@ -347,7 +354,7 @@
android:layout_marginStart="8dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="8dp"
- app:cardBackgroundColor="@color/pastelBlue"
+ app:cardBackgroundColor="@color/purple_200"
app:cardCornerRadius="10dp"
app:cardElevation="8dp"
app:layout_constraintEnd_toEndOf="parent"
@@ -382,6 +389,19 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
+
+
@@ -392,7 +412,7 @@
android:layout_marginStart="8dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="8dp"
- app:cardBackgroundColor="@color/pastelBlue"
+ app:cardBackgroundColor="@color/purple_200"
app:cardCornerRadius="10dp"
app:cardElevation="8dp"
app:layout_constraintEnd_toEndOf="parent"
@@ -437,7 +457,7 @@
android:layout_marginStart="8dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="8dp"
- app:cardBackgroundColor="@color/pastelBlue"
+ app:cardBackgroundColor="@color/purple_200"
app:cardCornerRadius="10dp"
app:cardElevation="8dp"
app:layout_constraintEnd_toEndOf="parent"
@@ -494,7 +514,7 @@
android:layout_marginStart="8dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="8dp"
- app:cardBackgroundColor="@color/pastelBlue"
+ app:cardBackgroundColor="@color/purple_200"
app:cardCornerRadius="10dp"
app:cardElevation="8dp"
app:layout_constraintEnd_toEndOf="parent"
@@ -544,13 +564,12 @@
-
-
-
+
+
+
-
+
-
+ app:layout_constraintTop_toBottomOf="@+id/dividerLine" />
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_recording.xml b/app/src/main/res/layout/fragment_recording.xml
index c04381a5..1755f744 100644
--- a/app/src/main/res/layout/fragment_recording.xml
+++ b/app/src/main/res/layout/fragment_recording.xml
@@ -1,145 +1,271 @@
-
+ android:layout_height="match_parent"
+ tools:context=".fragments.RecordingFragment">
-
-
-
-
+ android:layout_height="match_parent"
+ tools:context=".MapsActivity" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ app:layout_constraintBottom_toTopOf="@+id/stopButton"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:layout_constraintStart_toStartOf="parent" />
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+ android:layout_marginBottom="32dp"
+ android:clickable="true"
+ android:contentDescription="@string/floor_up_button"
+ android:src="@android:drawable/arrow_up_float"
+ app:layout_constraintBottom_toTopOf="@+id/floorDownButton"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.954"
+ app:layout_constraintStart_toStartOf="parent" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_replay.xml b/app/src/main/res/layout/fragment_replay.xml
index b6e88834..2a19fbbf 100644
--- a/app/src/main/res/layout/fragment_replay.xml
+++ b/app/src/main/res/layout/fragment_replay.xml
@@ -1,68 +1,113 @@
-
+ tools:context=".fragments.ReplayFragment">
-
-
+
-
+ android:layout_height="match_parent" />
-
-
+
+ android:layout_alignParentTop="true"
+ android:layout_alignParentEnd="true"
+ android:layout_margin="16dp"
+ android:background="@color/very_light_purple"
+ android:clickable="true"
+ android:elevation="10dp"
+ android:focusable="true"
+ android:orientation="vertical"
+ android:padding="12dp">
-
-
+
-
+ android:clickable="true"
+ android:focusable="true"
+ android:minHeight="48dp"
+ android:paddingStart="8dp"
+ android:paddingEnd="8dp"
+ android:text="GNSS"
+ android:textColor="@color/pastelBlue"
+ android:textSize="24sp"
+ android:textStyle="bold" />
-
+ android:clickable="true"
+ android:focusable="true"
+ android:minHeight="48dp"
+ android:paddingStart="8dp"
+ android:paddingEnd="8dp"
+ android:text="Fusion"
+ android:textColor="@android:color/holo_green_dark"
+ android:textSize="24sp"
+ android:textStyle="bold" />
+
+
+
+
+ android:layout_weight="1"
+ android:text="@string/play" />
+ android:layout_weight="1"
+ android:text="@string/restart" />
+
+
+
+
-
diff --git a/app/src/main/res/layout/fragment_startlocation.xml b/app/src/main/res/layout/fragment_startlocation.xml
index 61de731a..2f5b2e0b 100644
--- a/app/src/main/res/layout/fragment_startlocation.xml
+++ b/app/src/main/res/layout/fragment_startlocation.xml
@@ -16,41 +16,51 @@
+ tools:context=".fragments.CorrectionFragment">
-
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.0">
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/fragment_upload.xml b/app/src/main/res/layout/fragment_upload.xml
index 558a726f..1d7d4195 100644
--- a/app/src/main/res/layout/fragment_upload.xml
+++ b/app/src/main/res/layout/fragment_upload.xml
@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".presentation.fragment.UploadFragment">
+ tools:context=".fragments.UploadFragment">
+ android:layout_height="wrap_content">
-
-
+ android:layout_height="wrap_content"
+ android:paddingBottom="16dp">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_trajectorycard_view.xml b/app/src/main/res/layout/item_trajectorycard_view.xml
index ebbb9818..01565b95 100644
--- a/app/src/main/res/layout/item_trajectorycard_view.xml
+++ b/app/src/main/res/layout/item_trajectorycard_view.xml
@@ -1,49 +1,51 @@
-
-
-
+ app:cardBackgroundColor="@color/purple_200"
+ app:cardCornerRadius="20dp"
+ app:cardElevation="8dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+ android:layout_height="match_parent">
-
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+
-
+ app:layout_constraintEnd_toStartOf="@id/button_barrier"
+ app:layout_constraintStart_toEndOf="@id/trajectoryIdItem"
+ app:layout_constraintTop_toTopOf="parent" />
-
-
+ app:srcCompat="@drawable/ic_baseline_download_24" />
+
+
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_upload_card_view.xml b/app/src/main/res/layout/item_upload_card_view.xml
index 465ea27f..1224a27f 100644
--- a/app/src/main/res/layout/item_upload_card_view.xml
+++ b/app/src/main/res/layout/item_upload_card_view.xml
@@ -1,86 +1,88 @@
-
-
-
+ app:cardBackgroundColor="@color/purple_200"
+ app:cardCornerRadius="20dp"
+ app:cardElevation="8dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+ android:layout_height="match_parent">
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
-
+ app:layout_constraintTop_toTopOf="parent" />
-
-
-
-
-
+ app:srcCompat="@drawable/ic_baseline_upload_24" />
+
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_wifi_card_view.xml b/app/src/main/res/layout/item_wifi_card_view.xml
index ce457371..abd50cb7 100644
--- a/app/src/main/res/layout/item_wifi_card_view.xml
+++ b/app/src/main/res/layout/item_wifi_card_view.xml
@@ -1,21 +1,20 @@
+ android:layout_height="wrap_content">
-
-
+ android:layout_height="match_parent">
-
-
-
-
-
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_items.xml b/app/src/main/res/menu/menu_items.xml
index 350b8441..04acde72 100644
--- a/app/src/main/res/menu/menu_items.xml
+++ b/app/src/main/res/menu/menu_items.xml
@@ -6,7 +6,6 @@
android:id="@+id/settingsFragment"
android:icon="@drawable/ic_baseline_settings_24"
android:title="@string/settings_title"
- android:iconTint="@color/md_theme_light_onPrimary"
app:showAsAction="ifRoom"
/>
diff --git a/app/src/main/res/navigation/main_nav.xml b/app/src/main/res/navigation/main_nav.xml
index 9d966b29..d5641a89 100644
--- a/app/src/main/res/navigation/main_nav.xml
+++ b/app/src/main/res/navigation/main_nav.xml
@@ -7,10 +7,16 @@
-
+
+ app:popExitAnim="@anim/slide_out_right"/>
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
+
+
+
-
+ android:name="com.openpositioning.PositionMe.fragments.FilesFragment"
+ android:label="fragment_files"
+ tools:layout="@layout/fragment_files">
+
-
+
-
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
deleted file mode 100644
index 76fa7d44..00000000
--- a/app/src/main/res/values-night/colors.xml
+++ /dev/null
@@ -1,143 +0,0 @@
-
- #86D1EA
- #003642
- #004E5F
- #B3EBFF
- #B3CAD4
- #1D333B
- #344A52
- #CEE6F0
- #C1C4EB
- #2B2E4D
- #414465
- #DFE0FF
- #FFB4AB
- #690005
- #93000A
- #FFDAD6
- #0F1416
- #DEE3E6
- #0F1416
- #DEE3E6
- #40484B
- #BFC8CC
- #899296
- #40484B
- #000000
- #DEE3E6
- #2C3133
- #02677D
- #B3EBFF
- #001F27
- #86D1EA
- #004E5F
- #CEE6F0
- #061E25
- #B3CAD4
- #344A52
- #DFE0FF
- #151937
- #C1C4EB
- #414465
- #0F1416
- #343A3C
- #0A0F11
- #171C1F
- #1B2023
- #252B2D
- #303638
- #A0E7FF
- #002A34
- #4E9BB2
- #000000
- #C8E0EA
- #122930
- #7D949D
- #000000
- #D8DAFF
- #202342
- #8B8EB3
- #000000
- #FFD2CC
- #540003
- #FF5449
- #000000
- #0F1416
- #DEE3E6
- #0F1416
- #FFFFFF
- #40484B
- #D5DEE2
- #ABB3B7
- #899295
- #000000
- #DEE3E6
- #252B2D
- #004F60
- #B3EBFF
- #00141A
- #86D1EA
- #003C4A
- #CEE6F0
- #00141A
- #B3CAD4
- #233941
- #DFE0FF
- #0B0E2C
- #C1C4EB
- #303453
- #0F1416
- #404548
- #04080A
- #191E21
- #23292B
- #2E3336
- #393F41
- #D9F4FF
- #000000
- #83CDE6
- #000D12
- #DCF4FE
- #000000
- #AFC6D0
- #000D12
- #F0EEFF
- #000000
- #BDC0E7
- #050826
- #FFECE9
- #000000
- #FFAEA4
- #220001
- #0F1416
- #DEE3E6
- #0F1416
- #FFFFFF
- #40484B
- #FFFFFF
- #E9F1F5
- #BBC4C8
- #000000
- #DEE3E6
- #000000
- #004F60
- #B3EBFF
- #000000
- #86D1EA
- #00141A
- #CEE6F0
- #000000
- #B3CAD4
- #00141A
- #DFE0FF
- #000000
- #C1C4EB
- #0B0E2C
- #0F1416
- #4B5153
- #000000
- #1B2023
- #2C3133
- #373C3F
- #42484A
-
diff --git a/app/src/main/res/values-night/theme_overlays.xml b/app/src/main/res/values-night/theme_overlays.xml
deleted file mode 100644
index 02adac70..00000000
--- a/app/src/main/res/values-night/theme_overlays.xml
+++ /dev/null
@@ -1,98 +0,0 @@
-
-
-
-
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index f4253019..b0851002 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -1,9 +1,17 @@
-
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 3f28e98d..8e430fb1 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -13,154 +13,11 @@
#FF001080
#FF001080
#FE0000
+ #F8E6FF
+
+
+ #FF0000
+ #0000FF
+ #00FF00
-
- #6750A4
- #FFFFFF
- #625B71
- #FFFFFF
- #FFFBFE
- #FFFBFE
-
- #02677D
- #FFFFFF
- #B3EBFF
- #004E5F
- #4C626A
- #FFFFFF
- #CEE6F0
- #344A52
- #595C7E
- #FFFFFF
- #DFE0FF
- #414465
- #BA1A1A
- #FFFFFF
- #FFDAD6
- #93000A
- #F5FAFD
- #171C1F
- #F5FAFD
- #171C1F
- #DBE4E8
- #40484B
- #70787C
- #BFC8CC
- #000000
- #2C3133
- #ECF1F4
- #86D1EA
- #B3EBFF
- #001F27
- #86D1EA
- #004E5F
- #CEE6F0
- #061E25
- #B3CAD4
- #344A52
- #DFE0FF
- #151937
- #C1C4EB
- #414465
- #D6DBDD
- #F5FAFD
- #FFFFFF
- #EFF4F7
- #EAEFF1
- #E4E9EC
- #DEE3E6
- #003C4A
- #FFFFFF
- #22768D
- #FFFFFF
- #233941
- #FFFFFF
- #5A7179
- #FFFFFF
- #303453
- #FFFFFF
- #676B8D
- #FFFFFF
- #740006
- #FFFFFF
- #CF2C27
- #FFFFFF
- #F5FAFD
- #171C1F
- #F5FAFD
- #0C1214
- #DBE4E8
- #2F373B
- #4B5457
- #666E72
- #000000
- #2C3133
- #ECF1F4
- #86D1EA
- #22768D
- #FFFFFF
- #005D71
- #FFFFFF
- #5A7179
- #FFFFFF
- #425860
- #FFFFFF
- #676B8D
- #FFFFFF
- #4F5274
- #FFFFFF
- #C2C7CA
- #F5FAFD
- #FFFFFF
- #EFF4F7
- #E4E9EC
- #D8DEE0
- #CDD2D5
- #00313D
- #FFFFFF
- #005062
- #FFFFFF
- #192F36
- #FFFFFF
- #364C54
- #FFFFFF
- #262A48
- #FFFFFF
- #434767
- #FFFFFF
- #600004
- #FFFFFF
- #98000A
- #FFFFFF
- #F5FAFD
- #171C1F
- #F5FAFD
- #000000
- #DBE4E8
- #000000
- #252D31
- #424A4E
- #000000
- #2C3133
- #FFFFFF
- #86D1EA
- #005062
- #FFFFFF
- #003845
- #FFFFFF
- #364C54
- #FFFFFF
- #20363D
- #FFFFFF
- #434767
- #FFFFFF
- #2D304F
- #FFFFFF
- #B4BABC
- #F5FAFD
- #FFFFFF
- #ECF1F4
- #DEE3E6
- #D0D5D8
- #C2C7CA
\ No newline at end of file
diff --git a/app/src/main/res/values/googlemaps_api.xml b/app/src/main/res/values/googlemaps_api.xml
index 80672c61..12c34f16 100644
--- a/app/src/main/res/values/googlemaps_api.xml
+++ b/app/src/main/res/values/googlemaps_api.xml
@@ -1,6 +1,6 @@
- AIzaSyAGqo26Wz1SUnGYP3TDDxLSDNK-EvHHtNc
+ ${BuildConfig.MAPS_API_KEY}
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a2fa6043..2c954578 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -3,16 +3,16 @@
Done
- Complete
+ Stop
Start
Live data
- History
+ Files
See sensor hardware information
Refresh readings
Download
Grant permissions
Exit application
- OK
+ Ok
Show locally
Set
Cancel
@@ -43,10 +43,10 @@
ID
Date&Time
Elevation: %1s
- Computed Avg. Step Length
- unit: cm
+ Avg. Step Length
+ Enter corrected length
Long press and drag the marker to your start location
- Zoom, scroll and rotate the map \n to correct the path
+ Zoom, scroll and rotate the map to correct the path
Vendor:\t\t\t\t\t\t\t %1$s
@@ -110,31 +110,23 @@
Default building assumptions
Floor height in meters
Color
- 🛰 Show GNSS
+ GNSS
Floor Down button
Floor Up button
Choose Map
- ❇️ Auto Floor
+ Auto Floor
GNSS error:
Satellite
Normal
Hybrid
Delete
- \"Coordinates Start with Your First Move\"
- Sensor Info
- \"Coordinates Start with Your First Move\"
- Record
- Floor up button
- Floor down button
- Recording In Progress
- Correct your own avg step length:
- ...
- Path drawing
- Position Me
- GNSS is disabled
- Restart
- Play
- End
- Exit
+
+ Restart
+ Replay trajectory
+ Play
+ Pause
+ Current floor:
+ Current Floor
+ Fusion
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
deleted file mode 100644
index 528421a1..00000000
--- a/app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values/theme_overlays.xml b/app/src/main/res/values/theme_overlays.xml
deleted file mode 100644
index e81f18fb..00000000
--- a/app/src/main/res/values/theme_overlays.xml
+++ /dev/null
@@ -1,98 +0,0 @@
-
-
-
-
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index f4253019..f440d3dc 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,9 +1,29 @@
-
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index e208c000..7915fd1b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,7 +6,7 @@ buildscript {
}
dependencies {
// NOTE: Only classpath deps (plugins) go here
- classpath 'com.android.tools.build:gradle:8.8.0'
+ classpath 'com.android.tools.build:gradle:8.7.1'
classpath 'com.google.gms:google-services:4.4.2'
def nav_version = "2.5.3"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
diff --git a/secrets.properties b/secrets.properties
index f0dc54fd..bf8990fa 100644
--- a/secrets.properties
+++ b/secrets.properties
@@ -1,6 +1,6 @@
#
# Modify the variables to set your keys
#
-MAPS_API_KEY=
-OPENPOSITIONING_API_KEY=
-OPENPOSITIONING_MASTER_KEY=
+MAPS_API_KEY=
+OPENPOSITIONING_API_KEY=pj9uYBiMhyGaGKgLa6gn2A
+OPENPOSITIONING_MASTER_KEY=ewireless