From da83f48ba398bfd5be68000853bcf0e07c246ddb Mon Sep 17 00:00:00 2001 From: JunhongLiu <15202813786@163.com> Date: Sun, 8 Feb 2026 05:54:06 +0000 Subject: [PATCH 1/5] Complete Objective b --- app/build.gradle | 29 ++ .../data/remote/ServerCommunications.java | 139 +++++-- .../PositionMe/sensors/MovementSensor.java | 2 +- .../PositionMe/sensors/SensorFusion.java | 386 +++++++++++------- .../PositionMe/sensors/Wifi.java | 28 +- .../PositionMe/sensors/WifiDataProcessor.java | 117 +++--- .../PositionMe/utils/PathView.java | 20 + app/src/main/proto/new_traj.proto | 213 ++++++++++ build.gradle | 1 + 9 files changed, 699 insertions(+), 236 deletions(-) create mode 100644 app/src/main/proto/new_traj.proto diff --git a/app/build.gradle b/app/build.gradle index 3e29b13f..f8f5f4ee 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,6 +2,7 @@ plugins { id 'com.android.application' id 'com.google.gms.google-services' id 'androidx.navigation.safeargs' + id 'com.google.protobuf' id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' } @@ -55,8 +56,35 @@ android { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } + configurations { + all*.exclude group: 'com.google.protobuf', module: 'protobuf-javalite' + } + + sourceSets { + main { + proto { + + srcDir 'src/main/proto' + } + } + } } +protobuf { + protoc { + + artifact = "com.google.protobuf:protoc:3.21.7" + } + generateProtoTasks { + all().each { task -> + task.builtins { + java { + + } + } + } + } +} dependencies { // Core AndroidX implementation 'androidx.appcompat:appcompat:1.7.0-alpha03' // or stable: 1.6.1 @@ -92,4 +120,5 @@ dependencies { testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.2.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + implementation "com.google.protobuf:protobuf-java:3.21.7" } diff --git a/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java b/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java index 7f7e74b2..4e51c2df 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java +++ b/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java @@ -1,16 +1,4 @@ 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; import android.content.Context; import android.content.SharedPreferences; @@ -20,31 +8,39 @@ 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.presentation.fragment.FilesFragment; import com.openpositioning.PositionMe.sensors.Observable; import com.openpositioning.PositionMe.sensors.Observer; +import org.json.JSONObject; + +import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Files; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; -import java.util.zip.ZipEntry; +import java.util.Map; import java.util.zip.ZipInputStream; import okhttp3.Call; @@ -74,7 +70,7 @@ public class ServerCommunications implements Observable { public static Map downloadRecords = new HashMap<>(); // Application context for handling permissions and devices private final Context context; - + private Traj.Trajectory trajectory; // Network status checking private ConnectivityManager connMgr; private boolean isWifiConn; @@ -113,24 +109,94 @@ public ServerCommunications(Context context) { this.context = context; this.connMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); this.settings = PreferenceManager.getDefaultSharedPreferences(context); - this.isWifiConn = false; - this.isMobileConn = false; - checkNetworkStatus(); - this.observers = new ArrayList<>(); + } + public void sendInfo(Traj.Trajectory trajectory) { + this.trajectory = trajectory; - /** - * 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 - * to the API server. - * - * @param trajectory Traj object matching all the timing and formal restrictions. - */ - public void sendTrajectory(Traj.Trajectory trajectory){ - logDataSize(trajectory); + Log.i("ServerCommunications", "IMU Data size: " + trajectory.getImuDataCount()); + Log.i("ServerCommunications", "Light Data size: " + trajectory.getLightDataCount()); + Log.i("ServerCommunications", "GNSS Data size: " + trajectory.getGnssDataCount()); + + + Log.i("ServerCommunications", "WiFi Data size: " + trajectory.getWifiFingerprintsCount()); + + Log.i("ServerCommunications", "APS Data size: " + trajectory.getApsDataCount()); + Log.i("ServerCommunications", "PDR Data size: " + trajectory.getPdrDataCount()); + + + Log.i("ServerCommunications", "Mag Data size: " + trajectory.getMagnetometerDataCount()); + } + + + public void sendTrajectory(Map> sensorBuf, + Map wifiBuf, + Map gnssBuf, + Map pdrBuf, + Map apsBuf, + long start, String id){ + + Traj.Trajectory.Builder trajectoryBuilder = Traj.Trajectory.newBuilder(); + trajectoryBuilder.setStartTimestamp(start); + trajectoryBuilder.setTrajectoryId(id); + trajectoryBuilder.setAndroidVersion(String.valueOf(Build.VERSION.SDK_INT)); + + if(sensorBuf.get(0) != null) { + for(Object sample : sensorBuf.get(0).values()) { + trajectoryBuilder.addImuData((Traj.IMUReading) sample); + } + } + + if(sensorBuf.get(1) != null) { + for(Object sample : sensorBuf.get(1).values()) { + trajectoryBuilder.addMagnetometerData((Traj.MagnetometerReading) sample); + } + } + + if(sensorBuf.get(2) != null) { + for(Object sample : sensorBuf.get(2).values()) { + trajectoryBuilder.addPressureData((Traj.BarometerReading) sample); + } + } + + if(sensorBuf.get(3) != null) { + for(Object sample : sensorBuf.get(3).values()) { + trajectoryBuilder.addLightData((Traj.LightReading) sample); + } + } + + // PDR Data + if(pdrBuf != null) { + for(Object sample : pdrBuf.values()) { + trajectoryBuilder.addPdrData((Traj.RelativePosition) sample); + } + } + + // GNSS Data + if(gnssBuf != null) { + for(Object sample : gnssBuf.values()) { + trajectoryBuilder.addGnssData((Traj.GNSSReading) sample); + } + } + + // WiFi Data + if(wifiBuf != null) { + for(Object sample : wifiBuf.values()) { + trajectoryBuilder.addWifiFingerprints((Traj.Fingerprint) sample); + } + } + + // APs Data + if(apsBuf != null) { + for(Object sample : apsBuf.values()) { + trajectoryBuilder.addApsData((Traj.WiFiAPData) sample); + } + } + + this.trajectory = trajectoryBuilder.build(); + System.out.println("Trajectory created with ID: " + trajectory.getTrajectoryId()); - // Convert the trajectory to byte array byte[] binaryTrajectory = trajectory.toByteArray(); File path = null; @@ -523,6 +589,7 @@ public void onResponse(Call call, Response response) throws IOException { // Convert the byte array to protobuf byte[] byteArray = byteArrayOutputStream.toByteArray(); + Traj.Trajectory receivedTrajectory = Traj.Trajectory.parseFrom(byteArray); // Inspect the size of the received trajectory @@ -559,6 +626,8 @@ public void onResponse(Call call, Response response) throws IOException { loadDownloadRecords(); } } + + }); } @@ -623,11 +692,13 @@ 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", "Magnetometer Data size: " + trajectory.getMagnetometerDataCount()); 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", "WiFi Data size: " + trajectory.getWifiFingerprintsCount()); Log.i("ServerCommunications", "APS Data size: " + trajectory.getApsDataCount()); Log.i("ServerCommunications", "PDR Data size: " + trajectory.getPdrDataCount()); } @@ -664,4 +735,8 @@ else if (index == 1 && o instanceof MainActivity) { } } } + + + public void sendTrajectory(Traj.Trajectory sentTrajectory) { + } } \ No newline at end of file diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/MovementSensor.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/MovementSensor.java index a5282150..2cfb38b2 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/sensors/MovementSensor.java +++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/MovementSensor.java @@ -24,7 +24,7 @@ public class MovementSensor { protected Sensor sensor; // Information about the sensor stored in a SensorInfo object protected SensorInfo sensorInfo; - + public float[] values; /** * Public default constructor for the Movement Sensor class. 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..1f08e91e 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorFusion.java +++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorFusion.java @@ -17,16 +17,18 @@ import androidx.preference.PreferenceManager; import com.google.android.gms.maps.model.LatLng; +import com.openpositioning.PositionMe.Traj; +import com.openpositioning.PositionMe.data.remote.ServerCommunications; import com.openpositioning.PositionMe.presentation.activity.MainActivity; +import com.openpositioning.PositionMe.presentation.fragment.SettingsFragment; import com.openpositioning.PositionMe.utils.PathView; import com.openpositioning.PositionMe.utils.PdrProcessing; -import com.openpositioning.PositionMe.data.remote.ServerCommunications; -import com.openpositioning.PositionMe.Traj; -import com.openpositioning.PositionMe.presentation.fragment.SettingsFragment; import org.json.JSONException; import org.json.JSONObject; +import java.io.File; +import java.io.FileOutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -85,7 +87,6 @@ public class SensorFusion implements SensorEventListener, Observer { // Keep device awake while recording private PowerManager.WakeLock wakeLock; private Context appContext; - // Settings private SharedPreferences settings; @@ -110,7 +111,6 @@ public class SensorFusion implements SensorEventListener, Observer { private ServerCommunications serverCommunications; // Trajectory object containing all data private Traj.Trajectory.Builder trajectory; - // Settings private boolean saveRecording; private float filter_coefficient; @@ -158,6 +158,8 @@ public class SensorFusion implements SensorEventListener, Observer { private PathView pathView; // WiFi positioning object private WiFiPositioning wiFiPositioning; + private Timer timer; + //region Initialisation /** @@ -191,6 +193,7 @@ private SensorFusion() { this.R = new float[9]; // GNSS initial Long-Lat array this.startLocation = new float[2]; + } @@ -221,22 +224,22 @@ public void setContext(Context context) { this.appContext = context.getApplicationContext(); // store app context for later use // Initialise data collection devices (unchanged)... - this.accelerometerSensor = new MovementSensor(context, Sensor.TYPE_ACCELEROMETER); - this.barometerSensor = new MovementSensor(context, Sensor.TYPE_PRESSURE); - this.gyroscopeSensor = new MovementSensor(context, Sensor.TYPE_GYROSCOPE); - this.lightSensor = new MovementSensor(context, Sensor.TYPE_LIGHT); - this.proximitySensor = new MovementSensor(context, Sensor.TYPE_PROXIMITY); - this.magnetometerSensor = new MovementSensor(context, Sensor.TYPE_MAGNETIC_FIELD); - this.stepDetectionSensor = new MovementSensor(context, Sensor.TYPE_STEP_DETECTOR); - this.rotationSensor = new MovementSensor(context, Sensor.TYPE_ROTATION_VECTOR); - this.gravitySensor = new MovementSensor(context, Sensor.TYPE_GRAVITY); - this.linearAccelerationSensor = new MovementSensor(context, Sensor.TYPE_LINEAR_ACCELERATION); - // Listener based devices - this.wifiProcessor = new WifiDataProcessor(context); + this.accelerometerSensor = new MovementSensor(appContext, Sensor.TYPE_ACCELEROMETER); + this.barometerSensor = new MovementSensor(appContext, Sensor.TYPE_PRESSURE); + this.gyroscopeSensor = new MovementSensor(appContext, Sensor.TYPE_GYROSCOPE); + this.lightSensor = new MovementSensor(appContext, Sensor.TYPE_LIGHT); + this.proximitySensor = new MovementSensor(appContext, Sensor.TYPE_PROXIMITY); + this.magnetometerSensor = new MovementSensor(appContext, Sensor.TYPE_MAGNETIC_FIELD); + this.stepDetectionSensor = new MovementSensor(appContext, Sensor.TYPE_STEP_DETECTOR); + this.rotationSensor = new MovementSensor(appContext, Sensor.TYPE_ROTATION_VECTOR); + this.gravitySensor = new MovementSensor(appContext, Sensor.TYPE_GRAVITY); + this.linearAccelerationSensor = new MovementSensor(appContext, Sensor.TYPE_LINEAR_ACCELERATION); + + this.wifiProcessor = new WifiDataProcessor(appContext); wifiProcessor.registerObserver(this); - this.gnssProcessor = new GNSSDataProcessor(context, locationListener); - // Create object handling HTTPS communication - this.serverCommunications = new ServerCommunications(context); + this.gnssProcessor = new GNSSDataProcessor(appContext, locationListener); + this.serverCommunications = new ServerCommunications(appContext); + // Save absolute and relative start time this.absoluteStartTime = System.currentTimeMillis(); this.bootTime = SystemClock.uptimeMillis(); @@ -258,9 +261,10 @@ public void setContext(Context context) { // 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"); + if (powerManager != null) { + wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "PositionMe::SensorWakeLock"); + } } - //endregion //region Sensor processing @@ -275,36 +279,30 @@ 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"); -// } - } + long currentTime = System.currentTimeMillis(); // Current time in milliseconds + int sensorType = sensorEvent.sensor.getType(); // Update timestamp and frequency counter for this sensor lastEventTimestamps.put(sensorType, currentTime); eventCounts.put(sensorType, eventCounts.getOrDefault(sensorType, 0) + 1); - - switch (sensorType) { case Sensor.TYPE_ACCELEROMETER: + + if (accelerometerSensor != null) { + accelerometerSensor.values = sensorEvent.values.clone(); + } acceleration[0] = sensorEvent.values[0]; acceleration[1] = sensorEvent.values[1]; acceleration[2] = sensorEvent.values[2]; break; case Sensor.TYPE_PRESSURE: + if (barometerSensor != null) { + barometerSensor.values = sensorEvent.values.clone(); + } pressure = (1 - ALPHA) * pressure + ALPHA * sensorEvent.values[0]; if (saveRecording) { this.elevation = pdrProcessing.updateElevation( @@ -314,10 +312,35 @@ public void onSensorChanged(SensorEvent sensorEvent) { break; case Sensor.TYPE_GYROSCOPE: + + if (gyroscopeSensor != null) { + gyroscopeSensor.values = sensorEvent.values.clone(); + } angularVelocity[0] = sensorEvent.values[0]; angularVelocity[1] = sensorEvent.values[1]; angularVelocity[2] = sensorEvent.values[2]; + if (saveRecording && trajectory != null) { + + long relativeTime = System.currentTimeMillis() - absoluteStartTime; + + + Traj.IMUReading imuReading = Traj.IMUReading.newBuilder() + .setRelativeTimestamp(relativeTime) + .setGyr(Traj.Vector3.newBuilder() + .setX(angularVelocity[0]) + .setY(angularVelocity[1]) + .setZ(angularVelocity[2]) + .build()) + .build(); + + + synchronized (trajectory) { + trajectory.addImuData(imuReading); + } + } + break; + case Sensor.TYPE_LINEAR_ACCELERATION: filteredAcc[0] = sensorEvent.values[0]; filteredAcc[1] = sensorEvent.values[1]; @@ -351,7 +374,9 @@ public void onSensorChanged(SensorEvent sensorEvent) { break; case Sensor.TYPE_LIGHT: - light = sensorEvent.values[0]; + if (lightSensor != null) { + lightSensor.values = sensorEvent.values.clone(); + } break; case Sensor.TYPE_PROXIMITY: @@ -359,6 +384,10 @@ public void onSensorChanged(SensorEvent sensorEvent) { break; case Sensor.TYPE_MAGNETIC_FIELD: + + if (magnetometerSensor != null) { + magnetometerSensor.values = sensorEvent.values.clone(); + } magneticField[0] = sensorEvent.values[0]; magneticField[1] = sensorEvent.values[1]; magneticField[2] = sensorEvent.values[2]; @@ -406,10 +435,6 @@ public void onSensorChanged(SensorEvent sensorEvent) { if (saveRecording) { this.pathView.drawTrajectory(newCords); stepCounter++; - trajectory.addPdrData(Traj.Pdr_Sample.newBuilder() - .setRelativeTimestamp(SystemClock.uptimeMillis() - bootTime) - .setX(newCords[0]) - .setY(newCords[1])); } break; } @@ -435,29 +460,19 @@ public void logSensorFrequencies() { * Passed to the {@link GNSSDataProcessor} to receive the location data in this class. Save the * values in instance variables. */ - class myLocationListener implements LocationListener{ + 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)); + if (startLocation[0] == 0 && startLocation[1] == 0) { + startLocation[0] = latitude; + startLocation[1] = longitude; } } } - /** * {@inheritDoc} * @@ -469,18 +484,6 @@ public void onLocationChanged(@NonNull Location location) { public void update(Object[] wifiList) { // Save newest wifi values to local variable this.wifiList = Stream.of(wifiList).map(o -> (Wifi) o).collect(Collectors.toList()); - - if(this.saveRecording) { - Traj.WiFi_Sample.Builder wifiData = Traj.WiFi_Sample.newBuilder() - .setRelativeTimestamp(SystemClock.uptimeMillis()-bootTime); - for (Wifi data : this.wifiList) { - wifiData.addMacScans(Traj.Mac_Scan.newBuilder() - .setRelativeTimestamp(SystemClock.uptimeMillis() - bootTime) - .setMac(data.getBssid()).setRssi(data.getLevel())); - } - // Adding WiFi data to Trajectory - this.trajectory.addWifiData(wifiData); - } createWifiPositioningRequest(); } @@ -853,31 +856,57 @@ public void stopListening() { */ public void startRecording() { // If wakeLock is null (e.g. not initialized or was cleared), reinitialize it. - if (wakeLock == null) { + if (wakeLock == null && appContext != null) { PowerManager powerManager = (PowerManager) this.appContext.getSystemService(Context.POWER_SERVICE); - wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp::MyWakelockTag"); + wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "PositionMe::WakeLock"); + } + if (wakeLock != null && !wakeLock.isHeld()) { + wakeLock.acquire(31 * 60 * 1000L); } - wakeLock.acquire(31 * 60 * 1000L /*31 minutes*/); this.saveRecording = true; this.stepCounter = 0; this.absoluteStartTime = System.currentTimeMillis(); this.bootTime = SystemClock.uptimeMillis(); - // Protobuf trajectory class for sending sensor data to restful API - this.trajectory = Traj.Trajectory.newBuilder() - .setAndroidVersion(Build.VERSION.RELEASE) - .setStartTimestamp(absoluteStartTime) - .setAccelerometerInfo(createInfoBuilder(accelerometerSensor)) - .setGyroscopeInfo(createInfoBuilder(gyroscopeSensor)) - .setMagnetometerInfo(createInfoBuilder(magnetometerSensor)) - .setBarometerInfo(createInfoBuilder(barometerSensor)) - .setLightSensorInfo(createInfoBuilder(lightSensor)); + // Reset counters + this.counter = 0; + this.secondCounter = 0; + if (this.pathView != null) { + this.pathView.clearTrajectory(); + } + this.pdrProcessing.resetPDR(); + + // Initialize Traj Builder + this.trajectory = Traj.Trajectory.newBuilder(); + // Set Metadata + String currentTrajectoryIdtrajectoryId = "Traj_" + absoluteStartTime; + this.trajectory.setTrajectoryId(currentTrajectoryIdtrajectoryId); + this.trajectory.setAndroidVersion(Build.VERSION.RELEASE); + this.trajectory.setStartTimestamp(absoluteStartTime); + + if (accelerometerSensor != null) this.trajectory.setAccelerometerInfo(createSensorInfo(accelerometerSensor)); + if (gyroscopeSensor != null) this.trajectory.setGyroscopeInfo(createSensorInfo(gyroscopeSensor)); + if (magnetometerSensor != null) this.trajectory.setMagnetometerInfo(createSensorInfo(magnetometerSensor)); + if (barometerSensor != null) this.trajectory.setBarometerInfo(createSensorInfo(barometerSensor)); + if (lightSensor != null) this.trajectory.setLightSensorInfo(createSensorInfo(lightSensor)); + + // 4. Set Initial Position (Assignment 1 Requirement) + if (startLocation != null && (startLocation[0] != 0 || startLocation[1] != 0)) { + this.trajectory.setInitialPosition(Traj.GNSSPosition.newBuilder() + .setLatitude(startLocation[0]) + .setLongitude(startLocation[1]) + .build()); + } + // 5. Schedule Data Recording Task + if (storeTrajectoryTimer != null) { + storeTrajectoryTimer.cancel(); + } this.storeTrajectoryTimer = new Timer(); - this.storeTrajectoryTimer.schedule(new storeDataInTrajectory(), 0, TIME_CONST); - this.pdrProcessing.resetPDR(); + this.storeTrajectoryTimer.scheduleAtFixedRate(new storeDataInTrajectory(), 0, TIME_CONST); + if(settings.getBoolean("overwrite_constants", false)) { this.filter_coefficient = Float.parseFloat(settings.getString("accel_filter", "0.96")); } else { @@ -895,16 +924,36 @@ public void startRecording() { * @see SettingsFragment navigation that might cancel recording. */ public void stopRecording() { - // Only cancel if we are running - if(this.saveRecording) { + if (saveRecording) { + if (storeTrajectoryTimer != null) { + storeTrajectoryTimer.cancel(); + storeTrajectoryTimer = null; + } + if (wakeLock != null && wakeLock.isHeld()) { + wakeLock.release(); + } this.saveRecording = false; - storeTrajectoryTimer.cancel(); - } - if(wakeLock.isHeld()) { - this.wakeLock.release(); + + if (appContext != null && trajectory != null) { + try { + Traj.Trajectory finalTrajectory = this.trajectory.build(); + String fileName = "term_project_trajectory_" + absoluteStartTime + ".proto"; + + + File file = new File(appContext.getExternalFilesDir(null), fileName); + FileOutputStream fileOutputStream = new FileOutputStream(file); + fileOutputStream.write(finalTrajectory.toByteArray()); + fileOutputStream.close(); + + Log.d("SensorFusion", "Trajectory saved: " + file.getAbsolutePath()); + } catch (Exception e) { + Log.e("SensorFusion", "Error saving trajectory", e); + e.printStackTrace(); + } + } } - } + } //endregion //region Trajectory object @@ -915,14 +964,14 @@ public void stopRecording() { * @see ServerCommunications for sending and receiving data via HTTPS. */ public void sendTrajectoryToCloud() { - // Build object - Traj.Trajectory sentTrajectory = trajectory.build(); - // Pass object to communications object - this.serverCommunications.sendTrajectory(sentTrajectory); + if (trajectory != null) { + Traj.Trajectory sentTrajectory = trajectory.build(); + this.serverCommunications.sendTrajectory(sentTrajectory); + } } /** - * Creates a {@link Traj.Sensor_Info} objects from the specified sensor's data. + * Creates a {@link Traj.SensorInfo} objects from the specified sensor's data. * * @param sensor MovementSensor objects with populated sensorInfo fields * @return Traj.SensorInfo object to be used in building the trajectory @@ -930,14 +979,15 @@ public void sendTrajectoryToCloud() { * @see Traj Trajectory object used for communication with the server * @see MovementSensor class abstracting SensorManager based sensors */ - private Traj.Sensor_Info.Builder createInfoBuilder(MovementSensor sensor) { - return Traj.Sensor_Info.newBuilder() + private Traj.SensorInfo createSensorInfo(MovementSensor sensor) { + return Traj.SensorInfo.newBuilder() .setName(sensor.sensorInfo.getName()) .setVendor(sensor.sensorInfo.getVendor()) .setResolution(sensor.sensorInfo.getResolution()) .setPower(sensor.sensorInfo.getPower()) .setVersion(sensor.sensorInfo.getVersion()) - .setType(sensor.sensorInfo.getType()); + .setType(sensor.sensorInfo.getType()) + .build(); } /** @@ -947,65 +997,107 @@ private Traj.Sensor_Info.Builder createInfoBuilder(MovementSensor sensor) { * destroyed in {@link SensorFusion#stopRecording()}. */ private class storeDataInTrajectory extends TimerTask { + @Override 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) { + if (!saveRecording || trajectory == null) return; + + // Calculate temporal delta relative to system boot + long relativeTime = SystemClock.uptimeMillis() - bootTime; + + // Combine Acc, Gyro, Rotation, Steps into one IMUReading message + Traj.IMUReading.Builder imuBuilder = Traj.IMUReading.newBuilder() + .setStepCount(stepCounter); + + if (accelerometerSensor != null && accelerometerSensor.values != null) { + imuBuilder.setAcc(Traj.Vector3.newBuilder() + .setX(accelerometerSensor.values[0]) + .setY(accelerometerSensor.values[1]) + .setZ(accelerometerSensor.values[2]).build()); + } + + if (gyroscopeSensor != null && gyroscopeSensor.values != null) { + imuBuilder.setGyr(Traj.Vector3.newBuilder() + .setX(gyroscopeSensor.values[0]) + .setY(gyroscopeSensor.values[1]) + .setZ(gyroscopeSensor.values[2]).build()); + } + + // Handle Rotation Vector (x,y,z,w) + if (rotation != null && rotation.length >= 3) { + float w = (rotation.length > 3) ? rotation[3] : 1.0f; + imuBuilder.setRotationVector(Traj.Quaternion.newBuilder() + .setX(rotation[0]) + .setY(rotation[1]) + .setZ(rotation[2]) + .setW(w).build()); + } + + // Add IMU reading to trajectory + synchronized (trajectory) { + trajectory.addImuData(imuBuilder.build()); + } + + // Magnetometer Data + if (magnetometerSensor != null && magnetometerSensor.values != null) { + trajectory.addMagnetometerData(Traj.MagnetometerReading.newBuilder() + .setMag(Traj.Vector3.newBuilder() + .setX(magnetometerSensor.values[0]) + .setY(magnetometerSensor.values[1]) + .setZ(magnetometerSensor.values[2]).build()) + .build()); + } + + // Low Frequency Data (1Hz or slower) + if (counter >= 100) { // 100 * 10ms = 1000ms = 1s 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()); + + // Record Barometric Pressure + if (barometerSensor != null && barometerSensor.values != null) { + trajectory.addPressureData(Traj.BarometerReading.newBuilder() + .setPressure(barometerSensor.values[0]) + .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())); + // Record Ambient Light + if (lightSensor != null && lightSensor.values != null) { + trajectory.addLightData(Traj.LightReading.newBuilder() + .setLight(lightSensor.values[0]) + .build()); } - else { + + // Record GNSS (Every 1s) + if (latitude != 0 && longitude != 0) { + trajectory.addGnssData(Traj.GNSSReading.newBuilder() + .setPosition(Traj.GNSSPosition.newBuilder() + .setLatitude(latitude) + .setLongitude(longitude) + .setAltitude(elevation) + .build()) + .build()); + } + + // Sub-cycle for WiFi AP Data (Every 5s approx) + if (secondCounter >= 4) { // 5 * 1s = 5s + secondCounter = 0; + + if (wifiList != null && !wifiList.isEmpty()) { + for (Wifi wifi : wifiList) { + Traj.WiFiAPData.Builder wifiBuilder = Traj.WiFiAPData.newBuilder() + .setMac(wifi.getBssid()) + .setSsid(wifi.getSsid()) + .setFrequency(wifi.getFrequency()); + if (wifi.getRttFlag()) { + wifiBuilder.setRttEnabled(true); + } + trajectory.addApsData(wifiBuilder.build()); + } + } + } else { secondCounter++; } - } - else { + } else { counter++; } - } } 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..80322c38 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/sensors/Wifi.java +++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/Wifi.java @@ -18,6 +18,11 @@ public class Wifi { private int level; private long frequency; + // Add RTT support flag for IEEE 802.11mc capability + private boolean rttFlag; + + // Add unique identifier for scan traceability + private String uuid; /** * Empty public default constructor of the Wifi object. */ @@ -27,10 +32,17 @@ public Wifi(){} * Getters for each property */ public String getSsid() { return ssid; } - public long getBssid() { return bssid; } + public long getBssid() { return bssid; } public int getLevel() { return level; } public long getFrequency() { return frequency; } + // Getter for RTT flag + public boolean getRttFlag() { return rttFlag; } + + // Getter for unique ID + public String getUuid() { return uuid; } + + /** * Setters for each property */ @@ -39,6 +51,11 @@ public Wifi(){} public void setLevel(int level) { this.level = level; } public void setFrequency(long frequency) { this.frequency = frequency; } + // Setter for RTT flag + public void setRttFlag(boolean rttFlag) { this.rttFlag = rttFlag; } + + // Setter for unique ID + public void setUuid(String uuid) { this.uuid = uuid; } /** * Generates a string containing mac address and rssi of Wifi. * @@ -47,6 +64,13 @@ public Wifi(){} */ @Override public String toString() { - return "bssid: " + bssid +", level: " + level; + String macStr = String.format("%012X", bssid); // 转为16进制 + StringBuilder formattedMac = new StringBuilder(); + for (int i = 0; i < macStr.length(); i += 2) { + if (i > 0) formattedMac.append(":"); + formattedMac.append(macStr.substring(i, i + 2)); + } + + return "MAC: " + formattedMac.toString() + ", Level: " + level + "dBm, RTT: " + rttFlag; } } 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..13f8149a 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/sensors/WifiDataProcessor.java +++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/WifiDataProcessor.java @@ -16,9 +16,13 @@ import androidx.core.app.ActivityCompat; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Timer; import java.util.TimerTask; +import java.util.UUID; + /** * The WifiDataProcessor class is the Wi-Fi data gathering and processing class of the application. * It implements the wifi scanning and broadcasting design to identify a list of nearby Wi-Fis as @@ -101,48 +105,67 @@ public WifiDataProcessor(Context context) { * Receives updates when a wifi scan is complete. Observers are notified when the broadcast is * received to update the list of wifis */ + BroadcastReceiver wifiScanReceiver = new BroadcastReceiver() { - /** - * Updates the list of nearby wifis when the broadcast is received. - * Ensures wifi scans are not enabled if permissions are not granted. The list of wifis is - * then passed to store the Mac Address and strength and observers of the WifiDataProcessor - * class are notified of the updated wifi list. - * - * - * @param context Application Context to be used for permissions and device accesses. - * @param intent ???. - */ @Override public void onReceive(Context context, Intent intent) { if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { - // Unregister this listener - stopListening(); return; } - //Collect the list of nearby wifis - List wifiScanList = wifiManager.getScanResults(); - //Stop receiver as scan is complete - context.unregisterReceiver(this); - - //Loop though each item in wifi list - wifiData = new Wifi[wifiScanList.size()]; - for(int i = 0; i < wifiScanList.size(); i++) { - wifiData[i] = new Wifi(); - //Convert String mac address to an integer - String wifiMacAddress = wifiScanList.get(i).BSSID; - long intMacAddress = convertBssidToLong(wifiMacAddress); - //store mac address and rssi of wifi - wifiData[i].setBssid(intMacAddress); - wifiData[i].setLevel(wifiScanList.get(i).level); + boolean success = intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false); + if (success) { + List wifiScanList = wifiManager.getScanResults(); + + Map uniqueWifiMap = new HashMap<>(); + + for (ScanResult result : wifiScanList) { + long bssidLong = convertBssidToLong(result.BSSID); + + if (!uniqueWifiMap.containsKey(bssidLong)) { + Wifi wifi = new Wifi(); + wifi.setSsid(result.SSID); + wifi.setBssid(bssidLong); + wifi.setLevel(result.level); + wifi.setFrequency(result.frequency); + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + wifi.setRttFlag(result.is80211mcResponder()); + } else { + wifi.setRttFlag(false); + } + wifi.setUuid(UUID.randomUUID().toString()); + + uniqueWifiMap.put(bssidLong, wifi); + } + } + + Wifi currentConnectedWifi = getCurrentWifiData(); + if (currentConnectedWifi.getBssid() != 0) { + long currentBssid = currentConnectedWifi.getBssid(); + + if (!uniqueWifiMap.containsKey(currentBssid)) { + if (currentConnectedWifi.getUuid() == null) { + currentConnectedWifi.setUuid(UUID.randomUUID().toString()); + } + + uniqueWifiMap.put(currentBssid, currentConnectedWifi); + } + } + + + wifiData = uniqueWifiMap.values().toArray(new Wifi[0]); + notifyObservers(0); } - //Notify observers of change in wifiData variable - notifyObservers(0); + try { + context.unregisterReceiver(this); + } catch (IllegalArgumentException e) { + // Ignore + } } }; - /** * Converts mac address from string to integer. * Removes semicolons from mac address and converts each hex byte to a hex integer. @@ -153,30 +176,13 @@ public void onReceive(Context context, Intent intent) { * @return Long variable with decimal conversion of the mac address */ private long convertBssidToLong(String wifiMacAddress){ - long intMacAddress =0; - int colonCount =5; - //Loop through each character - for(int j =0; j<17; j++){ - //Identify character - char macByte = wifiMacAddress.charAt(j); - //convert string hex mac address with colons to decimal long integer - if(macByte != ':'){ - //For characters 0-9 subtract 48 from ASCII code and multiply by 16^position - if((int) macByte >= 48 && (int) macByte <= 57){ - intMacAddress = intMacAddress + (((int)macByte-48)*((long)Math.pow(16,16-j-colonCount))); - } - - //For characters a-f subtract 87 (=97-10) from ASCII code and multiply by 16^index - else if ((int) macByte >= 97 && (int) macByte <= 102){ - intMacAddress = intMacAddress + (((int)macByte-87)*((long)Math.pow(16,16-j-colonCount))); - } - } - else - //coloncount is used to obtain the index of each character - colonCount --; + if (wifiMacAddress == null || wifiMacAddress.isEmpty()) return 0; + try { + String hex = wifiMacAddress.replace(":", ""); + return Long.parseLong(hex, 16); + } catch (NumberFormatException e) { + return 0; } - - return intMacAddress; } /** @@ -313,13 +319,16 @@ public Wifi getCurrentWifiData(){ //Only obtain wifi data if the device is connected //Wifi in which the device is currently connected to Wifi currentWifi = new Wifi(); - if(networkInfo.isConnected()) { + if(networkInfo != null && networkInfo.isConnected()) { //Store the ssid, mac address and frequency of the current wifi currentWifi.setSsid(wifiManager.getConnectionInfo().getSSID()); String wifiMacAddress = wifiManager.getConnectionInfo().getBSSID(); + long intMacAddress = convertBssidToLong(wifiMacAddress); + currentWifi.setBssid(intMacAddress); currentWifi.setFrequency(wifiManager.getConnectionInfo().getFrequency()); + } else{ //Store standard information if not connected diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/PathView.java b/app/src/main/java/com/openpositioning/PositionMe/utils/PathView.java index 5a5efa8d..f071a8fc 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/utils/PathView.java +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/PathView.java @@ -244,4 +244,24 @@ public void redraw(float newScale){ reDraw = true; } + + public void clearTrajectory() { + + if (path != null) { + path.reset(); + } + + + if (xCoords != null) { + xCoords.clear(); + } + if (yCoords != null) { + yCoords.clear(); + } + + + invalidate(); + } + + } diff --git a/app/src/main/proto/new_traj.proto b/app/src/main/proto/new_traj.proto new file mode 100644 index 00000000..a9ed6190 --- /dev/null +++ b/app/src/main/proto/new_traj.proto @@ -0,0 +1,213 @@ +syntax = "proto3"; +option java_package = "com.openpositioning.PositionMe"; +option java_outer_classname = "Traj"; + +message Trajectory { + string android_version = 1; + // version 2.0 + float trajectory_version = 2; + // trajectory id/name for identification + string trajectory_id = 3; + repeated IMUReading imu_data = 4; + repeated RelativePosition pdr_data = 5; + repeated MagnetometerReading magnetometer_data = 6; + repeated BarometerReading pressure_data = 7; + repeated LightReading light_data = 8; + repeated ProximityReading proximity_data = 9; + + repeated GNSSReading gnss_data = 10; + repeated Fingerprint wifi_fingerprints = 11; + repeated WiFiAPData aps_data = 12; + repeated WiFiRTTReading wifi_rtt_data = 13; + repeated Fingerprint ble_fingerprints = 14; + repeated BleData ble_data = 15; + + // UNIX timestamp (in milliseconds) recorded from the start of this + // trajectory data collection event. All future + // timestamps in sub classes are to be RELATIVE timestamps + // (in milliseconds) to this start time. + // E.g. + // start_timestamp = 1674819807315 (UTC 27 Jan 2023 in the morning) + // relative_timestamp = 3000 (3s) + int64 start_timestamp = 16; + GNSSPosition initial_position = 17; + repeated GNSSPosition corrected_positions = 18; + + SensorInfo accelerometer_info = 19; + SensorInfo gyroscope_info = 20; + SensorInfo rotation_vector_info = 21; + SensorInfo magnetometer_info = 22; + SensorInfo barometer_info = 23; + SensorInfo light_sensor_info = 24; + SensorInfo proximity_info = 25; + + repeated GNSSPosition test_points = 26; + +} + +message RelativePosition { + // milliseconds from the start_timestamp + int64 relative_timestamp = 1; + + // Both in metres. You should implement an algorithm to estimate + // these values. The values are always relative to your start point + // so the first entry should always be x = 0.0, y = 0.0 + float x = 2; + float y = 3; +} + +message IMUReading { + // milliseconds + int64 relative_timestamp = 1; + // Accelerometer [m/s^2] + Vector3 acc = 2; + + // Gyroscope [radians/s] + Vector3 gyr = 3; + + // Orientation [unitless], 4 components should square sum to ~1 + Quaternion rotation_vector = 4; + + // Number of steps so far + int32 step_count = 5; +} + +message MagnetometerReading { + int64 relative_timestamp = 1; + + // Magnetometer [uT] + Vector3 mag = 2; +} + +message BarometerReading { + int64 relative_timestamp = 1; + + // mbar + float pressure = 2; +} + +message LightReading { + int64 relative_timestamp = 1; + // lux + float light = 2; +} + +message ProximityReading { + int64 relative_timestamp = 1; + // cm + float distance = 2; +} + +message GNSSPosition { + int64 relative_timestamp = 1; + + // degrees (minimum 6 significant figures) + // latitude between -90 and 90 + double latitude = 2; + // longitude between -180 and 180 + double longitude = 3; + //metres + double altitude = 4; + // floor name + optional string floor = 5; +} + +message GNSSReading { + GNSSPosition position = 1; + // metres + float accuracy = 2; + // m/s + float speed = 3; + // degrees + float bearing = 4; + + // e.g 'gps' or 'network' + string provider = 5; +} + +message Fingerprint { + int64 relative_timestamp = 1; + repeated RFScan rf_scans = 2; + +} + +message RFScan { + int64 relative_timestamp = 1; + + // Integer encoding of the hex mac address (BSSID) + // e.g. 207394925843984 + int64 mac = 2; + + // rssi integer in dBm. + // typically between -120 and -10 + int32 rssi = 3; + + // returned position + optional GNSSPosition position = 4; +} + +message WiFiRTTReading { + int64 relative_timestamp = 1; + // cm + // Integer encoding of the hex mac address (BSSID) + // e.g. 207394925843984 + int64 mac = 2; + + // in mm + float distance = 3; + // in mm + float distance_std = 4; + // rssi integer in dBm. + // typically between -120 and -10 + int32 rssi = 5; +} + +message WiFiAPData { + // Integer encoding of the hex mac address (BSSID) + // e.g. 207394925843984 + int64 mac = 1; + + // E.g. 'Eduroam' or 'Starbucks_free_wifi' + string ssid = 2; + + // Typically 2.4GHz or 5GHz + int64 frequency = 3; + + // Flag to indicate if the AP supports RTT measurements + bool rtt_enabled = 4; +} + +message BleData { + string mac_address = 1; + string name = 2; + int32 tx_power_level = 3; + int32 advertise_flags = 4; + repeated string service_uuids = 5; + bytes manufacturer_data = 6; +} + + // --- Common Types --- +message Vector3 { + float x = 1; + float y = 2; + float z = 3; +} + +message Quaternion { + float x = 1; + float y = 2; + float z = 3; + float w = 4; +} + +message SensorInfo { + string name = 1; + string vendor = 2; + float resolution = 3; + float power = 4; + int32 version = 5; + int32 type = 6; + float max_range = 7; + float frequency = 8; +} + diff --git a/build.gradle b/build.gradle index e208c000..65574938 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,7 @@ buildscript { // NOTE: Only classpath deps (plugins) go here classpath 'com.android.tools.build:gradle:8.8.0' classpath 'com.google.gms:google-services:4.4.2' + classpath "com.google.protobuf:protobuf-gradle-plugin:0.9.4" def nav_version = "2.5.3" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1" From 8be3b45999a25708d6a24774a24e118a3d875062 Mon Sep 17 00:00:00 2001 From: Zhenghong Zhong <1368338295@qq.com> Date: Sun, 8 Feb 2026 23:04:41 +0000 Subject: [PATCH 2/5] Add test point tagging and write test_points to protobuf. Complete Objective C --- app/.gitignore | 13 +++- .../data/remote/ServerCommunications.java | 12 +++- .../fragment/RecordingFragment.java | 68 ++++++++++++++++++- .../fragment/TrajectoryMapFragment.java | 60 ++++++++++++++++ .../PositionMe/sensors/SensorFusion.java | 27 ++++++++ .../main/res/layout/fragment_recording.xml | 31 ++++++++- build.gradle | 2 +- 7 files changed, 206 insertions(+), 7 deletions(-) diff --git a/app/.gitignore b/app/.gitignore index 42afabfd..2f67ba3e 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1 +1,12 @@ -/build \ No newline at end of file +/build +# NEW: do not commit secrets +secrets.properties + +# NEW: do not commit generated/build outputs +**/build/ + +# NEW: do not commit generated protobuf Java +**/Traj.java + +# NEW: old proto should not be committed (we use new_traj.proto) +app/src/main/proto/traj.proto diff --git a/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java b/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java index 4e51c2df..44008250 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java +++ b/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java @@ -20,6 +20,7 @@ import com.openpositioning.PositionMe.presentation.fragment.FilesFragment; import com.openpositioning.PositionMe.sensors.Observable; import com.openpositioning.PositionMe.sensors.Observer; +import com.openpositioning.PositionMe.sensors.SensorFusion; import org.json.JSONObject; @@ -138,9 +139,18 @@ public void sendTrajectory(Map> sensorBuf, long start, String id){ Traj.Trajectory.Builder trajectoryBuilder = Traj.Trajectory.newBuilder(); - trajectoryBuilder.setStartTimestamp(start); + // Set trajectory start timestamp (UNIX ms) for relative timestamps. + trajectoryBuilder.setStartTimestamp(SensorFusion.getInstance().getStartTimestampMs()); + // Attach Add-Tag test points to protobuf + trajectoryBuilder.addAllTestPoints(SensorFusion.getInstance().getTestPoints()); trajectoryBuilder.setTrajectoryId(id); trajectoryBuilder.setAndroidVersion(String.valueOf(Build.VERSION.SDK_INT)); + trajectoryBuilder.setStartTimestamp(SensorFusion.getInstance().getStartTimestampMs()); + trajectoryBuilder.addAllTestPoints(SensorFusion.getInstance().getTestPoints()); + // Debug log to verify test_points are attached + android.util.Log.d("TestPoints", "Attached test_points count = " + trajectoryBuilder.getTestPointsCount()); + + if(sensorBuf.get(0) != null) { for(Object sample : sensorBuf.get(0).values()) { 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 index 6362a971..dc6acd5e 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/RecordingFragment.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/RecordingFragment.java @@ -59,7 +59,7 @@ public class RecordingFragment extends Fragment { // UI elements - private MaterialButton completeButton, cancelButton; + private MaterialButton completeButton, cancelButton,addTagButton; private ImageView recIcon; private ProgressBar timeRemaining; private TextView elevation, distanceTravelled, gnssError; @@ -80,6 +80,15 @@ public class RecordingFragment extends Fragment { // References to the child map fragment private TrajectoryMapFragment trajectoryMapFragment; + // Add Tag counter + private int tagCount = 0; + + // Save the test point of the user pressing "Add Tag" + private final java.util.ArrayList testPoints = + new java.util.ArrayList<>(); + // Timestamp + private long startTimestampMs = 0L; + private final Runnable refreshDataTask = new Runnable() { @Override public void run() { @@ -116,6 +125,15 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + // Reset test-point state at the beginning of a new recording session + testPoints.clear(); // clear local list + tagCount = 0; // reset label counter + startTimestampMs = System.currentTimeMillis(); // reset start time + + // Also reset SensorFusion copies + sensorFusion.setTestPoints(testPoints); + sensorFusion.setStartTimestampMs(startTimestampMs); + // Child Fragment: the container in fragment_recording.xml // where TrajectoryMapFragment is placed trajectoryMapFragment = (TrajectoryMapFragment) @@ -137,6 +155,9 @@ public void onViewCreated(@NonNull View view, completeButton = view.findViewById(R.id.stopButton); cancelButton = view.findViewById(R.id.cancelButton); + addTagButton = view.findViewById(R.id.addTagButton); + addTagButton.bringToFront(); + addTagButton.setElevation(20f); recIcon = view.findViewById(R.id.redDot); timeRemaining = view.findViewById(R.id.timeRemainingBar); @@ -147,6 +168,14 @@ public void onViewCreated(@NonNull View view, // Buttons completeButton.setOnClickListener(v -> { + // Pass the start time and testPoints to SensorFusion + sensorFusion.setStartTimestampMs(startTimestampMs); + sensorFusion.setTestPoints(testPoints); + + //Debug - verify test points are passed into SensorFusion before stopRecording(). + android.util.Log.d("TestPoints", "Before stop: local testPoints size = " + testPoints.size()); + android.util.Log.d("TestPoints", "Before stop: sensorFusion testPoints size = " + sensorFusion.getTestPoints().size()); + // Stop recording & go to correction if (autoStop != null) autoStop.cancel(); sensorFusion.stopRecording(); @@ -181,6 +210,43 @@ public void onViewCreated(@NonNull View view, dialog.show(); // Finally, show the dialog }); + // Button Click Listener + addTagButton.setOnClickListener(v -> { + tagCount++; + + LatLng current = null; + if (trajectoryMapFragment != null) { + current = trajectoryMapFragment.getCurrentLocation(); + } + + if (current != null && trajectoryMapFragment != null) { + // 1) Map displaying numbered points + trajectoryMapFragment.addTagPoint(current, tagCount); + + // 2) Calculate relative timestamp (ms) + long relativeTs = System.currentTimeMillis() - startTimestampMs; + + // 3) Assemble a protobuf 'GNSSPosition'(the element type of `test_points`) + com.openpositioning.PositionMe.Traj.GNSSPosition p = + com.openpositioning.PositionMe.Traj.GNSSPosition.newBuilder() + .setRelativeTimestamp(relativeTs) + .setLatitude(current.latitude) + .setLongitude(current.longitude) + .setAltitude((double) sensorFusion.getElevation()) + .build(); + + testPoints.add(p); + + android.util.Log.d("TestPoints", + "Saved test point #" + tagCount + + " ts=" + relativeTs + + " lat=" + current.latitude + + " lon=" + current.longitude); + } else { + android.util.Log.d("TestPoints", "Add Tag clicked but current location is null"); + } + }); + // The blinking effect for recIcon blinkingRecordingIcon(); 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 index eb0bad65..654bb944 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/TrajectoryMapFragment.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/TrajectoryMapFragment.java @@ -25,6 +25,14 @@ import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.SupportMapFragment; import com.google.android.gms.maps.model.*; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import androidx.core.content.ContextCompat; +import com.google.android.gms.maps.model.BitmapDescriptor; +import com.google.android.gms.maps.model.BitmapDescriptorFactory; +import com.google.android.gms.maps.model.MarkerOptions; import java.util.ArrayList; import java.util.List; @@ -80,6 +88,48 @@ public class TrajectoryMapFragment extends Fragment { private com.google.android.material.floatingactionbutton.FloatingActionButton floorUpButton, floorDownButton; private Button switchColorButton; private Polygon buildingPolygon; + // Number Marker + private BitmapDescriptor createNumberedMarkerIcon(int number) { + final int size = 56; + Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + + // Circular background + Paint circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + circlePaint.setStyle(Paint.Style.FILL); + circlePaint.setColor(0xFF2196F3); + + float cx = size / 2f; + float cy = size / 2f; + float r = size / 2f; + canvas.drawCircle(cx, cy, r, circlePaint); + + // White outline + Paint strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + strokePaint.setStyle(Paint.Style.STROKE); + strokePaint.setStrokeWidth(3f); + strokePaint.setColor(0xFFFFFFFF); + canvas.drawCircle(cx, cy, r - 3f, strokePaint); + + // number + Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + textPaint.setColor(0xFFFFFFFF); + textPaint.setTextAlign(Paint.Align.CENTER); + textPaint.setFakeBoldText(true); + textPaint.setTextSize(25f); + + String text = String.valueOf(number); + + // Make text vertically centered + Rect textBounds = new Rect(); + textPaint.getTextBounds(text, 0, text.length(), textBounds); + float textY = cy + textBounds.height() / 2f; + + canvas.drawText(text, cx, textY, textPaint); + + return BitmapDescriptorFactory.fromBitmap(bitmap); + } + public TrajectoryMapFragment() { @@ -327,6 +377,16 @@ public void updateUserLocation(@NonNull LatLng newLocation, float orientation) { } } + // Add numbered label points on the map + public void addTagPoint(@NonNull LatLng latLng, int index) { + if (gMap == null) return; + + gMap.addMarker(new MarkerOptions() + .position(latLng) + .anchor(0.5f, 0.5f) + .icon(createNumberedMarkerIcon(index))); + } + /** 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 1f08e91e..0e582ea5 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorFusion.java +++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorFusion.java @@ -159,6 +159,11 @@ public class SensorFusion implements SensorEventListener, Observer { // WiFi positioning object private WiFiPositioning wiFiPositioning; private Timer timer; + // Record test points (Add Tag) + private final java.util.ArrayList testPoints = + new java.util.ArrayList<>(); + // Record recording start time (ms) + private long startTimestampMs = 0L; //region Initialisation @@ -631,6 +636,23 @@ public void onAccuracyChanged(Sensor sensor, int i) {} //endregion //region Getters/Setters + public void setStartTimestampMs(long ts) { + this.startTimestampMs = ts; + } + + public long getStartTimestampMs() { + return this.startTimestampMs; + } + + public void setTestPoints(java.util.List points) { + this.testPoints.clear(); + if (points != null) this.testPoints.addAll(points); + } + + public java.util.List getTestPoints() { + return new java.util.ArrayList<>(this.testPoints); + } + /** * Getter function for core location data. * @@ -880,6 +902,11 @@ public void startRecording() { // Initialize Traj Builder this.trajectory = Traj.Trajectory.newBuilder(); + // Attach Add-Tag test points to protobuf + this.trajectory.addAllTestPoints(getTestPoints()); + // Debug log to verify test_points are attached. + android.util.Log.d("TestPoints", "SensorFusion builder test_points count = " + this.trajectory.getTestPointsCount()); + // Set Metadata String currentTrajectoryIdtrajectoryId = "Traj_" + absoluteStartTime; this.trajectory.setTrajectoryId(currentTrajectoryIdtrajectoryId); diff --git a/app/src/main/res/layout/fragment_recording.xml b/app/src/main/res/layout/fragment_recording.xml index c04381a5..6693cca6 100644 --- a/app/src/main/res/layout/fragment_recording.xml +++ b/app/src/main/res/layout/fragment_recording.xml @@ -58,7 +58,6 @@ android:layout_marginStart="16dp" android:layout_marginEnd="16dp" /> - + app:layout_constraintBottom_toTopOf="@id/controlLayout"> + + + + + + + + + + diff --git a/build.gradle b/build.gradle index 65574938..1eeb4711 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' classpath "com.google.protobuf:protobuf-gradle-plugin:0.9.4" def nav_version = "2.5.3" From b2a3b94c1961ddc640d9111b66375753b6417ff6 Mon Sep 17 00:00:00 2001 From: Cheng Qian Date: Tue, 10 Feb 2026 20:15:48 +0000 Subject: [PATCH 3/5] In Door Map (Task D) --- .../PositionMe/data/remote/Building.java | 27 + .../PositionMe/data/remote/FloorPlan.java | 32 + .../data/remote/ServerCommunications.java | 621 +++++++++++------- .../presentation/fragment/HomeFragment.java | 11 +- .../fragment/IndoorMapDisplayFragment.java | 275 ++++++++ .../PositionMe/sensors/SensorFusion.java | 12 +- app/src/main/res/layout/fragment_home.xml | 6 +- .../main/res/layout/fragment_indoor_map.xml | 56 ++ app/src/main/res/navigation/main_nav.xml | 19 +- 9 files changed, 827 insertions(+), 232 deletions(-) create mode 100644 app/src/main/java/com/openpositioning/PositionMe/data/remote/Building.java create mode 100644 app/src/main/java/com/openpositioning/PositionMe/data/remote/FloorPlan.java create mode 100644 app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/IndoorMapDisplayFragment.java create mode 100644 app/src/main/res/layout/fragment_indoor_map.xml diff --git a/app/src/main/java/com/openpositioning/PositionMe/data/remote/Building.java b/app/src/main/java/com/openpositioning/PositionMe/data/remote/Building.java new file mode 100644 index 00000000..0415395a --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/data/remote/Building.java @@ -0,0 +1,27 @@ +package com.openpositioning.PositionMe.data.remote; + +import java.util.List; + +/** + * Data model representing a Building. + * Contains the building's metadata, outline (polygon), and a list of floor plans. + */ +public class Building { + private String id; + private String name; + // Outline points: List of [Latitude, Longitude] + private List> outline; + private List floors; + + public Building(String id, String name, List> outline, List floors) { + this.id = id; + this.name = name; + this.outline = outline; + this.floors = floors; + } + + public String getId() { return id; } + public String getName() { return name; } + public List> getOutline() { return outline; } + public List getFloors() { return floors; } +} \ No newline at end of file diff --git a/app/src/main/java/com/openpositioning/PositionMe/data/remote/FloorPlan.java b/app/src/main/java/com/openpositioning/PositionMe/data/remote/FloorPlan.java new file mode 100644 index 00000000..4a48b173 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/data/remote/FloorPlan.java @@ -0,0 +1,32 @@ +package com.openpositioning.PositionMe.data.remote; + +import java.util.List; + +/** + * Data model representing a single floor plan. + * Updated to support Vector Data (Walls) from API. + */ +public class FloorPlan { + private String floorCode; // e.g., "1", "G", "B1" + private int order; + private String imageUrl; // Keep this for compatibility, though API might not send it + private double[] bounds; + + // 新增:存储墙体线条数据 + // 结构:List of Lines, each Line is a List of Points [Lat, Lon] + private List>> walls; + + public FloorPlan(String floorCode, int order, String imageUrl, double[] bounds, List>> walls) { + this.floorCode = floorCode; + this.order = order; + this.imageUrl = imageUrl; + this.bounds = bounds; + this.walls = walls; + } + + public String getFloorCode() { return floorCode; } + public int getOrder() { return order; } + public String getImageUrl() { return imageUrl; } + public double[] getBounds() { return bounds; } + public List>> getWalls() { return walls; } +} \ No newline at end of file diff --git a/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java b/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java index 44008250..aaa703bd 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java +++ b/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java @@ -12,10 +12,9 @@ import android.widget.Toast; import androidx.preference.PreferenceManager; - +import com.openpositioning.PositionMe.Traj; import com.google.protobuf.util.JsonFormat; import com.openpositioning.PositionMe.BuildConfig; -import com.openpositioning.PositionMe.Traj; import com.openpositioning.PositionMe.presentation.activity.MainActivity; import com.openpositioning.PositionMe.presentation.fragment.FilesFragment; import com.openpositioning.PositionMe.sensors.Observable; @@ -56,6 +55,14 @@ import okhttp3.Response; import okhttp3.ResponseBody; + +// 在现有的 imports 下方添加: +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import org.json.JSONArray; +import org.json.JSONException; +import java.util.Collections; +import java.util.Comparator; /** * This class handles communications with the server through HTTPs. The class uses an * {@link OkHttpClient} for making requests to the server. The class includes methods for sending @@ -68,11 +75,16 @@ * @author Mate Stodulka */ public class ServerCommunications implements Observable { + // ============================================================== + // 部分 1:变量定义与 Log 标签 + // ============================================================== + + // 1. 添加日志标签 (在 Logcat 中搜索 "ServerDebug" 就能看到日志) + private static final String TAG = "ServerDebug"; + public static Map downloadRecords = new HashMap<>(); - // Application context for handling permissions and devices private final Context context; private Traj.Trajectory trajectory; - // Network status checking private ConnectivityManager connMgr; private boolean isWifiConn; private boolean isMobileConn; @@ -82,9 +94,11 @@ public class ServerCommunications implements Observable { private boolean success; private List observers; - // Static constants necessary for communications - private static final String userKey = BuildConfig.OPENPOSITIONING_API_KEY; - private static final String masterKey = BuildConfig.OPENPOSITIONING_MASTER_KEY; + // 2. 必须硬编码 Key 以修复 401 错误 + private static final String userKey = "LY31NlnGAe9vN-HvQJWTZg"; + private static final String masterKey = "ewireless"; + + // 3. URL 定义 private static final String uploadURL = "https://openpositioning.org/api/live/trajectory/upload/" + userKey + "/?key=" + masterKey; @@ -94,9 +108,15 @@ public class ServerCommunications implements Observable { private static final String infoRequestURL = "https://openpositioning.org/api/live/users/trajectories/" + userKey + "?key=" + masterKey; + + // Task D: 室内地图请求 URL + private static final String floorPlanRequestURL = + "https://openpositioning.org/api/live/floorplan/request/" + userKey + + "?key=" + masterKey; + private static final String PROTOCOL_CONTENT_TYPE = "multipart/form-data"; private static final String PROTOCOL_ACCEPT_TYPE = "application/json"; - + // ... (下面接着是你原来的 public ServerCommunications(Context context) 构造函数,不要动) /** @@ -104,7 +124,7 @@ public class ServerCommunications implements Observable { * initialises a {@link ConnectivityManager}, {@link Observer} and gets the user preferences. * Boolean variables storing WiFi and Mobile Data connection status are initialised to false. * - * @param context application context for handling permissions and devices. + * @param context application context for handling permissions and devices. */ public ServerCommunications(Context context) { this.context = context; @@ -113,6 +133,7 @@ public ServerCommunications(Context context) { this.observers = new ArrayList<>(); } + public void sendInfo(Traj.Trajectory trajectory) { this.trajectory = trajectory; @@ -131,216 +152,87 @@ public void sendInfo(Traj.Trajectory trajectory) { } - public void sendTrajectory(Map> sensorBuf, - Map wifiBuf, - Map gnssBuf, - Map pdrBuf, - Map apsBuf, - long start, String id){ - - Traj.Trajectory.Builder trajectoryBuilder = Traj.Trajectory.newBuilder(); - // Set trajectory start timestamp (UNIX ms) for relative timestamps. - trajectoryBuilder.setStartTimestamp(SensorFusion.getInstance().getStartTimestampMs()); - // Attach Add-Tag test points to protobuf - trajectoryBuilder.addAllTestPoints(SensorFusion.getInstance().getTestPoints()); - trajectoryBuilder.setTrajectoryId(id); - trajectoryBuilder.setAndroidVersion(String.valueOf(Build.VERSION.SDK_INT)); - trajectoryBuilder.setStartTimestamp(SensorFusion.getInstance().getStartTimestampMs()); - trajectoryBuilder.addAllTestPoints(SensorFusion.getInstance().getTestPoints()); - // Debug log to verify test_points are attached - android.util.Log.d("TestPoints", "Attached test_points count = " + trajectoryBuilder.getTestPointsCount()); - - - - if(sensorBuf.get(0) != null) { - for(Object sample : sensorBuf.get(0).values()) { - trajectoryBuilder.addImuData((Traj.IMUReading) sample); - } - } - - if(sensorBuf.get(1) != null) { - for(Object sample : sensorBuf.get(1).values()) { - trajectoryBuilder.addMagnetometerData((Traj.MagnetometerReading) sample); - } + /** + * 上传轨迹到服务器 (Feature B & D) + * 替换了旧的 Map 参数版本,直接接收构建好的 Protobuf 对象和 Campaign 名称 + */ + public void sendTrajectory(Traj.Trajectory sentTrajectory, String campaign) { + // 1. 处理 Campaign 参数 (Feature D 要求) + // 如果未指定,默认为 murchison_house,防止 URL 错误 + if (campaign == null || campaign.isEmpty()) { + campaign = "murchison_house"; } - if(sensorBuf.get(2) != null) { - for(Object sample : sensorBuf.get(2).values()) { - trajectoryBuilder.addPressureData((Traj.BarometerReading) sample); - } - } + // 2. 动态构建 URL + // 格式: .../upload/{campaign}/{userKey}/?key={masterKey} + String dynamicUrl = "https://openpositioning.org/api/live/trajectory/upload/" + campaign + "/" + userKey + "/?key=" + masterKey; - if(sensorBuf.get(3) != null) { - for(Object sample : sensorBuf.get(3).values()) { - trajectoryBuilder.addLightData((Traj.LightReading) sample); - } + // 3. 将 Protobuf 对象写入临时文件 (OkHttp 需要文件流) + File file; + try { + // 使用时间戳防止文件名冲突 + String fileName = "upload_" + System.currentTimeMillis() + ".proto"; + // 使用缓存目录,避免污染外部存储 + file = new File(context.getCacheDir(), fileName); + + FileOutputStream fos = new FileOutputStream(file); + fos.write(sentTrajectory.toByteArray()); + fos.close(); + } catch (IOException e) { + e.printStackTrace(); + System.err.println("Failed to save temp file for upload"); + return; } - // PDR Data - if(pdrBuf != null) { - for(Object sample : pdrBuf.values()) { - trajectoryBuilder.addPdrData((Traj.RelativePosition) sample); - } - } + // 4. 构建网络请求 (Multipart Upload) + OkHttpClient client = new OkHttpClient(); - // GNSS Data - if(gnssBuf != null) { - for(Object sample : gnssBuf.values()) { - trajectoryBuilder.addGnssData((Traj.GNSSReading) sample); - } - } + RequestBody requestBody = new MultipartBody.Builder().setType(MultipartBody.FORM) + .addFormDataPart("file", file.getName(), + RequestBody.create(MediaType.parse("application/octet-stream"), file)) + .build(); - // WiFi Data - if(wifiBuf != null) { - for(Object sample : wifiBuf.values()) { - trajectoryBuilder.addWifiFingerprints((Traj.Fingerprint) sample); - } - } + Request request = new Request.Builder() + .url(dynamicUrl) + .post(requestBody) + .build(); - // APs Data - if(apsBuf != null) { - for(Object sample : apsBuf.values()) { - trajectoryBuilder.addApsData((Traj.WiFiAPData) sample); - } - } + System.out.println("Uploading to: " + dynamicUrl); - this.trajectory = trajectoryBuilder.build(); - System.out.println("Trajectory created with ID: " + trajectory.getTrajectoryId()); + // 5. 异步发送请求 + client.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + e.printStackTrace(); + System.err.println("Upload Failed: " + e.getMessage()); - byte[] binaryTrajectory = trajectory.toByteArray(); + // 通知 MainActivity 更新 UI (失败) + success = false; + notifyObservers(1); - 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(); + // 清理临时文件 + if (file.exists()) file.delete(); } - } else { // for android 12 or lower use internal storage - path = context.getFilesDir(); - } - - System.out.println(path.toString()); - - // Format the file name according to date - SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yy-HH-mm-ss"); - Date date = new Date(); - File file = new File(path, "trajectory_" + dateFormat.format(date) + ".txt"); - try { - // Write the binary data to the file - FileOutputStream stream = new FileOutputStream(file); - stream.write(binaryTrajectory); - stream.close(); - System.out.println("Recorded binary trajectory for debugging stored in: " + path); - } catch (IOException ee) { - // Catch and print if writing to the file fails - System.err.println("Storing of recorded binary trajectory failed: " + ee.getMessage()); - } - - // Check connections available before sending data - checkNetworkStatus(); - - // Check if user preference allows for syncing with mobile data - // TODO: add sync delay and enforce settings - boolean enableMobileData = this.settings.getBoolean("mobile_sync", false); - // Check if device is connected to WiFi or to mobile data with enabled preference - if(this.isWifiConn || (enableMobileData && isMobileConn)) { - // Instantiate client for HTTP requests - OkHttpClient client = new OkHttpClient(); - - // 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)) - .build(); - - // Create a POST request with the required headers - Request request = new 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() { - - // Handle failure to get response from the server - @Override public void onFailure(Call call, IOException e) { - e.printStackTrace(); - System.err.println("Failure to get response"); - // Delete the local file and set success to false - //file.delete(); - success = false; - 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); - } + @Override + public void onResponse(Call call, Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + if (!response.isSuccessful()) { + System.err.println("Upload Error: " + response.code() + " " + responseBody.string()); + success = false; + } else { + System.out.println("Upload SUCCESS: " + responseBody.string()); + success = true; } - } - - // Process the server's response - @Override public void onResponse(Call call, Response response) throws IOException { - try (ResponseBody responseBody = response.body()) { - // If the response is unsuccessful, delete the local file and throw an - // exception - if (!response.isSuccessful()) { - //file.delete(); -// 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 - - System.err.println("POST error response: " + errorBody); - success = false; - notifyObservers(1); - throw new IOException("Unexpected code " + response); - } - // Print the response headers - Headers responseHeaders = response.headers(); - for (int i = 0, size = responseHeaders.size(); i < size; i++) { - System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i)); - } - // 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()); - } + // 通知 MainActivity 更新 UI (根据 success 状态) + notifyObservers(1); - // Delete local file and set success to true - success = file.delete(); - notifyObservers(1); - } + // 清理临时文件 + if (file.exists()) file.delete(); } - }); - } - else { - // If the device is not connected to network or allowed to send, do not send trajectory - // and notify observers and user - System.err.println("No uploading allowed right now!"); - success = false; - notifyObservers(1); - } + } + }); } /** @@ -475,9 +367,9 @@ private void loadDownloadRecords() { * 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 + * @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); @@ -543,8 +435,8 @@ private void saveDownloadRecord(long startTimestamp, String fileName, String id, * trajectory to be downloaded. The trajectory is then converted to a protobuf object and * 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 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) { @@ -570,7 +462,8 @@ public void onFailure(Call call, IOException e) { @Override public void onResponse(Call call, Response response) throws IOException { try (ResponseBody responseBody = response.body()) { - if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); + if (!response.isSuccessful()) + throw new IOException("Unexpected code " + response); // Extract the nth entry from the zip InputStream inputStream = responseBody.byteStream(); @@ -645,7 +538,6 @@ public void onResponse(Call call, Response response) throws IOException { /** * API request for information about submitted trajectories. If the response is successful, * the {@link ServerCommunications#infoResponse} field is updated and observes notified. - * */ public void sendInfoRequest() { // Create a new OkHttpclient @@ -660,11 +552,13 @@ public void sendInfoRequest() { // Enqueue the GET request for asynchronous execution client.newCall(request).enqueue(new okhttp3.Callback() { - @Override public void onFailure(Call call, IOException e) { + @Override + public void onFailure(Call call, IOException e) { e.printStackTrace(); } - @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()) { // Check if the response is successful if (!response.isSuccessful()) throw new IOException("Unexpected code " + @@ -672,7 +566,7 @@ public void sendInfoRequest() { // Get the requested information from the response body and save it in a string // TODO: add printing to the screen somewhere - infoResponse = responseBody.string(); + infoResponse = responseBody.string(); // Print a message in the console and notify observers System.out.println("Response received"); notifyObservers(0); @@ -715,7 +609,7 @@ private void logDataSize(Traj.Trajectory trajectory) { /** * {@inheritDoc} - * + *

* Implement default method from Observable Interface to add new observers to the list of * registered observers. * @@ -728,7 +622,7 @@ public void registerObserver(Observer o) { /** * {@inheritDoc} - * + *

* Method for notifying all registered observers. The observer is notified based on the index * passed to the method. * @@ -736,17 +630,304 @@ 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}); + for (Observer o : observers) { + if (index == 0 && o instanceof FilesFragment) { + o.update(new String[]{infoResponse}); + } else if (index == 1 && o instanceof MainActivity) { + o.update(new Boolean[]{success}); + } + } + } + // ========================================== + // 部分 2:Task D 功能实现 (带 Logcat 调试) + // ========================================== + + public interface BuildingCallback { + void onBuildingsReceived(List buildings); + void onError(String message); + } + + public interface ImageCallback { + void onImageLoaded(Bitmap bitmap); + void onError(String message); + } + + public void getNearbyBuildings(double lat, double lng, BuildingCallback callback) { + OkHttpClient client = new OkHttpClient(); + + // [Debug] 打印请求 URL 和参数 + Log.d(TAG, "------------------------------------------------"); + Log.d(TAG, "Requesting Buildings..."); + Log.d(TAG, "URL: " + floorPlanRequestURL); + Log.d(TAG, "Coordinates: " + lat + ", " + lng); + + JSONObject jsonBody = new JSONObject(); + try { + jsonBody.put("lat", lat); + jsonBody.put("lon", lng); + jsonBody.put("macs", new JSONArray()); + Log.d(TAG, "Request Body: " + jsonBody.toString()); + } catch (JSONException e) { + e.printStackTrace(); + return; + } + + MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + RequestBody body = RequestBody.create(JSON, jsonBody.toString()); + + Request request = new Request.Builder() + .url(floorPlanRequestURL) + .post(body) // 必须是 POST + .addHeader("accept", "application/json") + .build(); + + client.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + // [Debug] 网络请求失败 + Log.e(TAG, "Network FAILURE: " + e.getMessage()); + new Handler(Looper.getMainLooper()).post(() -> + callback.onError("Network Error: " + e.getMessage())); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + // [Debug] 收到服务器响应 + Log.d(TAG, "Response Code: " + response.code()); + + try (ResponseBody responseBody = response.body()) { + if (!response.isSuccessful()) { + // [Debug] 打印错误详情 (例如 401 Unauthorized) + String errorMsg = responseBody != null ? responseBody.string() : "null"; + Log.e(TAG, "Server Error Body: " + errorMsg); + new Handler(Looper.getMainLooper()).post(() -> + callback.onError("Server Error " + response.code() + ": " + errorMsg)); + return; + } + + String jsonString = responseBody.string(); + // [Debug] 打印成功的 JSON 数据 + Log.d(TAG, "Response JSON: " + jsonString); + + try { + List buildings = parseBuildingsJson(jsonString); + Log.d(TAG, "Parsed Buildings Count: " + buildings.size()); + new Handler(Looper.getMainLooper()).post(() -> + callback.onBuildingsReceived(buildings)); + } catch (JSONException e) { + Log.e(TAG, "JSON Parsing Error: " + e.getMessage()); + new Handler(Looper.getMainLooper()).post(() -> + callback.onError("Parsing Error: " + e.getMessage())); + } + } + } + }); + } + + public void downloadFloorMapImage(String url, ImageCallback callback) { + Log.d(TAG, "Downloading Image from: " + url); + OkHttpClient client = new OkHttpClient(); + Request request = new Request.Builder().url(url).build(); + + client.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + Log.e(TAG, "Image Download Failure: " + e.getMessage()); + new Handler(Looper.getMainLooper()).post(() -> + callback.onError("Image Download Failed")); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + if (response.isSuccessful() && response.body() != null) { + InputStream inputStream = response.body().byteStream(); + Bitmap bitmap = BitmapFactory.decodeStream(inputStream); + Log.d(TAG, "Image Downloaded & Decoded. Size: " + bitmap.getByteCount()); + new Handler(Looper.getMainLooper()).post(() -> + callback.onImageLoaded(bitmap)); + } else { + Log.e(TAG, "Image Response Error: " + response.code()); + new Handler(Looper.getMainLooper()).post(() -> + callback.onError("Image Response Failed")); + } + } + }); + } + + /** + * 修复后的解析方法:支持解析 GeoJSON 格式的 outline 字符串 + */ + /** + * 修复后的解析方法:同时支持 outline 和 map_shapes (GeoJSON 格式) + */ + /** + * 修复后的解析方法:加入楼层智能排序 (B1 < G < 1 < 2 ...) + */ + /** + * 修复后的解析方法:支持任意 GeoJSON 形状 (LineString, MultiLineString, Polygon) + */ + private List parseBuildingsJson(String jsonString) throws JSONException { + List buildingList = new ArrayList<>(); + JSONArray jsonArray = new JSONArray(jsonString); + + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject bObj = jsonArray.getJSONObject(i); + + String id = bObj.has("id") && !bObj.isNull("id") ? bObj.getString("id") : "unknown"; + String name = bObj.has("name") ? bObj.getString("name") : "Unknown Building"; + + // 1. 解析 Outline + List> outline = new ArrayList<>(); + if (bObj.has("outline")) { + Object outlineObj = bObj.get("outline"); + if (outlineObj instanceof String) { + // 提取轮廓 + extractPolygonsFromGeoJson((String) outlineObj, outline); + } } - else if (index == 1 && o instanceof MainActivity) { - o.update(new Boolean[] {success}); + + // 2. 解析 Floors + List floors = new ArrayList<>(); + if (bObj.has("map_shapes")) { + String mapShapesStr = bObj.getString("map_shapes"); + JSONObject mapShapes = new JSONObject(mapShapesStr); + + Iterator keys = mapShapes.keys(); + while (keys.hasNext()) { + String floorCode = keys.next(); + double[] bounds = new double[]{0, 0, 0, 0}; + + // 🔴 关键修改:提取墙体 (walls) + List>> walls = new ArrayList<>(); + Object floorData = mapShapes.get(floorCode); + + // 无论数据是 JSONObject 还是 String,都转成 String 处理 + String floorGeoJson = floorData instanceof JSONObject ? floorData.toString() : floorData.toString(); + + // 使用新的增强版解析器 + extractWallsFromGeoJson(floorGeoJson, walls); + + floors.add(new FloorPlan(floorCode, 0, null, bounds, walls)); + } } + + // 排序楼层 + Collections.sort(floors, new Comparator() { + @Override + public int compare(FloorPlan f1, FloorPlan f2) { + return Integer.compare(getFloorOrderValue(f1.getFloorCode()), getFloorOrderValue(f2.getFloorCode())); + } + }); + + buildingList.add(new Building(id, name, outline, floors)); } + return buildingList; + } + + /** + * 辅助方法:楼层排序权重 + */ + private int getFloorOrderValue(String code) { + if (code == null) return 0; + String raw = code.trim().toUpperCase(); + if (raw.equals("G") || raw.equals("GROUND") || raw.equals("0")) return 0; + if (raw.equals("LG") || raw.startsWith("B")) return -1; + try { return Integer.parseInt(raw); } catch (NumberFormatException e) { return 0; } + } + + /** + * 辅助方法:提取 Outline (仅多边形) + */ + private void extractPolygonsFromGeoJson(String geoJsonStr, List> outList) { + try { + JSONObject featureCollection = new JSONObject(geoJsonStr); + JSONArray features = featureCollection.getJSONArray("features"); + if (features.length() > 0) { + JSONObject geometry = features.getJSONObject(0).getJSONObject("geometry"); + JSONArray coordinates = geometry.getJSONArray("coordinates"); + String type = geometry.getString("type"); + + JSONArray ring = null; + if (type.equalsIgnoreCase("MultiPolygon")) ring = coordinates.getJSONArray(0).getJSONArray(0); + else if (type.equalsIgnoreCase("Polygon")) ring = coordinates.getJSONArray(0); + + if (ring != null) { + for (int j = 0; j < ring.length(); j++) { + JSONArray point = ring.getJSONArray(j); + List latLng = new ArrayList<>(); + latLng.add(point.getDouble(1)); + latLng.add(point.getDouble(0)); + outList.add(latLng); + } + } + } + } catch (Exception e) { e.printStackTrace(); } } + /** + * 🔴 增强版墙体解析器:支持 LineString, MultiLineString 和 Polygon + */ + private void extractWallsFromGeoJson(String geoJsonStr, List>> wallsList) { + try { + JSONObject featureCollection = new JSONObject(geoJsonStr); + JSONArray features = featureCollection.getJSONArray("features"); + + for (int i = 0; i < features.length(); i++) { + JSONObject feature = features.getJSONObject(i); + if (!feature.has("geometry")) continue; + + JSONObject geometry = feature.getJSONObject("geometry"); + String type = geometry.getString("type"); + JSONArray coordinates = geometry.getJSONArray("coordinates"); + + // 情况 1: MultiLineString (多条线) -> [[[lon,lat], [lon,lat]], ...] + if (type.equalsIgnoreCase("MultiLineString")) { + for (int k = 0; k < coordinates.length(); k++) { + parseLineString(coordinates.getJSONArray(k), wallsList); + } + } + // 情况 2: LineString (单条线) -> [[lon,lat], [lon,lat], ...] + else if (type.equalsIgnoreCase("LineString")) { + parseLineString(coordinates, wallsList); + } + // 情况 3: Polygon (多边形房间) -> [[[lon,lat], ...]] + else if (type.equalsIgnoreCase("Polygon")) { + // Polygon 的第一个元素是外环,当作线条画出来 + parseLineString(coordinates.getJSONArray(0), wallsList); + } + // 情况 4: MultiPolygon + else if (type.equalsIgnoreCase("MultiPolygon")) { + for (int k = 0; k < coordinates.length(); k++) { + parseLineString(coordinates.getJSONArray(k).getJSONArray(0), wallsList); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + Log.e(TAG, "Wall Parsing Error: " + e.getMessage()); + } + } + // 解析单个 LineString 的坐标数组 + private void parseLineString(JSONArray lineArray, List>> wallsList) throws JSONException { + List> path = new ArrayList<>(); + for (int p = 0; p < lineArray.length(); p++) { + JSONArray point = lineArray.getJSONArray(p); + List latLng = new ArrayList<>(); + latLng.add(point.getDouble(1)); // Lat + latLng.add(point.getDouble(0)); // Lon + path.add(latLng); + } + if (!path.isEmpty()) { + wallsList.add(path); + } + } + /** + * 兼容性方法:修复 SensorFusion 报错 + */ public void sendTrajectory(Traj.Trajectory sentTrajectory) { + Log.d(TAG, "sendTrajectory (compatibility overload) called"); + this.sendTrajectory(sentTrajectory, "murchison_house"); } -} \ 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 index 8371b04e..62dfe55b 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/HomeFragment.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/HomeFragment.java @@ -50,6 +50,8 @@ public class HomeFragment extends Fragment implements OnMapReadyCallback { private Button start; private Button measurements; private Button files; + // Task D: New button for Indoor Map + private MaterialButton indoorMapButton; private TextView gnssStatusTextView; // For the map @@ -116,6 +118,13 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat Navigation.findNavController(v).navigate(action); }); + // Task D: Indoor Map Button Logic + indoorMapButton = view.findViewById(R.id.indoorButton); + indoorMapButton.setOnClickListener(v -> { + // Navigate to IndoorMapDisplayFragment using the ID defined in main_nav.xml + Navigation.findNavController(v).navigate(R.id.action_homeFragment_to_indoorMapDisplayFragment); + }); + // TextView to display GNSS disabled message gnssStatusTextView = view.findViewById(R.id.gnssStatusTextView); @@ -215,4 +224,4 @@ private void checkAndUpdatePermissions() { showEdinburghAndMessage("GNSS is disabled. Please enable in settings."); } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/IndoorMapDisplayFragment.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/IndoorMapDisplayFragment.java new file mode 100644 index 00000000..897e0d35 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/IndoorMapDisplayFragment.java @@ -0,0 +1,275 @@ +package com.openpositioning.PositionMe.presentation.fragment; + +import android.content.Context; +import android.content.SharedPreferences; +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.Button; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceManager; + +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.Polygon; +import com.google.android.gms.maps.model.PolygonOptions; +import com.google.android.gms.maps.model.Polyline; +import com.google.android.gms.maps.model.PolylineOptions; +import com.openpositioning.PositionMe.R; +import com.openpositioning.PositionMe.data.remote.Building; +import com.openpositioning.PositionMe.data.remote.FloorPlan; +import com.openpositioning.PositionMe.data.remote.ServerCommunications; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class IndoorMapDisplayFragment extends Fragment implements OnMapReadyCallback { + + private static final String TAG = "IndoorMapFragment"; + + // UI Components + private GoogleMap mMap; + private TextView floorText; + private Button btnUp, btnDown; + + // Logic Components + private ServerCommunications serverCommunications; + private Building selectedBuilding; + private int currentFloorIndex = 0; + private List currentFloorLines = new ArrayList<>(); + private Set loadedBuildingNames = new HashSet<>(); + + // Locations + private static final LatLng LOC_NUCLEUS = new LatLng(55.9232, -3.1742); + private static final LatLng LOC_MURCHISON = new LatLng(55.924131, -3.179167); + private static final LatLng CAMERA_CENTER = new LatLng(55.9236, -3.1767); + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_indoor_map, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + floorText = view.findViewById(R.id.floorText); + btnUp = view.findViewById(R.id.btnUp); + btnDown = view.findViewById(R.id.btnDown); + updateUIState(false); + + serverCommunications = new ServerCommunications(requireContext()); + + SupportMapFragment mapFragment = (SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.mapContainer); + if (mapFragment != null) { + mapFragment.getMapAsync(this); + } + + btnUp.setOnClickListener(v -> changeFloor(1)); + btnDown.setOnClickListener(v -> changeFloor(-1)); + } + + @Override + public void onMapReady(@NonNull GoogleMap googleMap) { + mMap = googleMap; + mMap.setMapType(GoogleMap.MAP_TYPE_HYBRID); + mMap.getUiSettings().setZoomControlsEnabled(true); + mMap.getUiSettings().setCompassEnabled(true); + mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(CAMERA_CENTER, 16.5f)); + + requestAllBuildings(); + + mMap.setOnPolygonClickListener(polygon -> { + Building building = (Building) polygon.getTag(); + if (building != null) { + onBuildingSelected(building); + } + }); + + // Long press to manually scan a location + mMap.setOnMapLongClickListener(latLng -> { + Toast.makeText(getContext(), "Scanning location...", Toast.LENGTH_SHORT).show(); + serverCommunications.getNearbyBuildings(latLng.latitude, latLng.longitude, new ServerCommunications.BuildingCallback() { + @Override + public void onBuildingsReceived(List buildings) { + if (!buildings.isEmpty()) { + addBuildingsToMap(buildings); + Toast.makeText(getContext(), "Found: " + buildings.get(0).getName(), Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(getContext(), "No buildings found here", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onError(String message) { + Log.e(TAG, "Manual scan error: " + message); + } + }); + }); + } + + private void requestAllBuildings() { + loadedBuildingNames.clear(); + mMap.clear(); + + // Step 1: Request Nucleus + serverCommunications.getNearbyBuildings(LOC_NUCLEUS.latitude, LOC_NUCLEUS.longitude, new ServerCommunications.BuildingCallback() { + @Override + public void onBuildingsReceived(List buildings) { + addBuildingsToMap(buildings); + requestMurchison(); + } + + @Override + public void onError(String message) { + Log.e(TAG, "Nucleus request failed: " + message); + requestMurchison(); + } + }); + } + + private void requestMurchison() { + // Step 2: Request Murchison + serverCommunications.getNearbyBuildings(LOC_MURCHISON.latitude, LOC_MURCHISON.longitude, new ServerCommunications.BuildingCallback() { + @Override + public void onBuildingsReceived(List buildings) { + addBuildingsToMap(buildings); + } + + @Override + public void onError(String message) { + Log.e(TAG, "Murchison request failed: " + message); + } + }); + } + + private void addBuildingsToMap(List newBuildings) { + if (mMap == null) return; + + for (Building b : newBuildings) { + String name = (b.getName() != null) ? b.getName() : "Unknown"; + if (loadedBuildingNames.contains(name)) continue; + + loadedBuildingNames.add(name); + drawSingleBuildingOutline(b); + } + } + + private void drawSingleBuildingOutline(Building building) { + if (building.getOutline() == null || building.getOutline().isEmpty()) return; + + List points = new ArrayList<>(); + for (List point : building.getOutline()) { + if (point.size() >= 2) points.add(new LatLng(point.get(0), point.get(1))); + } + + int strokeColor = Color.RED; + int fillColor = Color.argb(50, 255, 0, 0); + + String name = building.getName().toLowerCase(); + if (name.contains("nucleus")) { + strokeColor = Color.rgb(255, 191, 0); // Amber + fillColor = Color.argb(50, 255, 191, 0); + } else if (name.contains("fleeming") || name.contains("jenkin")) { + strokeColor = Color.BLUE; + fillColor = Color.argb(50, 0, 0, 255); + } + + Polygon polygon = mMap.addPolygon(new PolygonOptions() + .addAll(points) + .strokeColor(strokeColor) + .fillColor(fillColor) + .strokeWidth(5) + .clickable(true)); + polygon.setTag(building); + } + + private void onBuildingSelected(Building building) { + this.selectedBuilding = building; + + // Save selected venue (campaign) to SharedPreferences + String venueName = building.getName(); + String campaignName = venueName.toLowerCase().replace(" ", "_"); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); + prefs.edit().putString("current_campaign", campaignName).apply(); + + Toast.makeText(getContext(), "Venue set to: " + venueName, Toast.LENGTH_SHORT).show(); + + // Select default floor (G or 0) + this.currentFloorIndex = 0; + for (int i = 0; i < building.getFloors().size(); i++) { + String code = building.getFloors().get(i).getFloorCode(); + if (code.equalsIgnoreCase("G") || code.equals("0")) { + this.currentFloorIndex = i; + break; + } + } + + updateUIState(true); + if (!building.getFloors().isEmpty()) { + drawCurrentFloorWalls(); + } + } + + private void changeFloor(int delta) { + if (selectedBuilding == null || selectedBuilding.getFloors().isEmpty()) return; + + int newIndex = currentFloorIndex + delta; + if (newIndex < 0) newIndex = 0; + if (newIndex >= selectedBuilding.getFloors().size()) newIndex = selectedBuilding.getFloors().size() - 1; + + if (newIndex != currentFloorIndex) { + currentFloorIndex = newIndex; + drawCurrentFloorWalls(); + } + } + + private void drawCurrentFloorWalls() { + // Clear previous lines + for (Polyline line : currentFloorLines) { + line.remove(); + } + currentFloorLines.clear(); + + FloorPlan floor = selectedBuilding.getFloors().get(currentFloorIndex); + floorText.setText("Floor: " + floor.getFloorCode()); + + if (floor.getWalls() != null) { + for (List> wallPath : floor.getWalls()) { + List points = new ArrayList<>(); + for (List point : wallPath) { + points.add(new LatLng(point.get(0), point.get(1))); + } + + Polyline line = mMap.addPolyline(new PolylineOptions() + .addAll(points) + .color(Color.YELLOW) + .width(6) + .zIndex(100)); + + currentFloorLines.add(line); + } + } + } + + private void updateUIState(boolean isVisible) { + int v = isVisible ? View.VISIBLE : View.GONE; + if (floorText != null) floorText.setVisibility(v); + if (btnUp != null) btnUp.setVisibility(v); + if (btnDown != null) btnDown.setVisibility(v); + } +} \ No newline at end of file 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 0e582ea5..66620a41 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorFusion.java +++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorFusion.java @@ -964,9 +964,13 @@ public void stopRecording() { if (appContext != null && trajectory != null) { try { Traj.Trajectory finalTrajectory = this.trajectory.build(); - String fileName = "term_project_trajectory_" + absoluteStartTime + ".proto"; + if (serverCommunications != null) { + Log.d("SensorFusion", "Uploading trajectory..."); + serverCommunications.sendTrajectory(finalTrajectory, "murchison_house"); + } + String fileName = "term_project_trajectory_" + absoluteStartTime + ".proto"; File file = new File(appContext.getExternalFilesDir(null), fileName); FileOutputStream fileOutputStream = new FileOutputStream(file); fileOutputStream.write(finalTrajectory.toByteArray()); @@ -993,7 +997,7 @@ public void stopRecording() { public void sendTrajectoryToCloud() { if (trajectory != null) { Traj.Trajectory sentTrajectory = trajectory.build(); - this.serverCommunications.sendTrajectory(sentTrajectory); + this.serverCommunications.sendTrajectory(sentTrajectory,"murchison_house"); } } @@ -1114,8 +1118,8 @@ public void run() { .setSsid(wifi.getSsid()) .setFrequency(wifi.getFrequency()); if (wifi.getRttFlag()) { - wifiBuilder.setRttEnabled(true); - } + wifiBuilder.setRttEnabled(true); + } trajectory.addApsData(wifiBuilder.build()); } } diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 99c1ef13..c32db7a9 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -155,13 +155,13 @@ + + + + + + +