diff --git a/.gitignore b/.gitignore index d4c3a57e..132f1095 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ .cxx local.properties /.idea/ +*.bak diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0b33fdb8..08ff4fee 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -30,7 +30,7 @@ android:theme="@style/Theme.Cloud"> + android:value=""/> Fleeming_POLYGON = Arrays.asList( + new LatLng(55.9221059, -3.1723130), // Southwest + new LatLng(55.9222226, -3.1719519), // Southeast + new LatLng(55.9228053, -3.1726003), // Northeast + new LatLng(55.9226930, -3.1729124), // Northwest + new LatLng(55.9221059, -3.1723130) // **Close boundary** + ); + + public static final LatLng Hudson_CENTER = new LatLng(55.9225085, -3.1713467); + + // Define Hudson Building Polygon (manually adjusted based on Google Maps) + public static final List Hudson_POLYGON = Arrays.asList( + new LatLng(55.9223633, -3.1715301), // Southwest + new LatLng(55.9225434, -3.1710165), // Southeast + new LatLng(55.9226656, -3.1711522), // Northeast + new LatLng(55.9224837, -3.1716374), // Northwest + new LatLng(55.9223633, -3.1715301) // **Close boundary** + ); public static final List NUCLEUS_POLYGON = new ArrayList() {{ add(BuildingPolygon.NUCLEUS_NE); add(new LatLng(BuildingPolygon.NUCLEUS_SW.latitude, BuildingPolygon.NUCLEUS_NE.longitude)); // South-East add(BuildingPolygon.NUCLEUS_SW); add(new LatLng(BuildingPolygon.NUCLEUS_NE.latitude, BuildingPolygon.NUCLEUS_SW.longitude)); // North-West + }}; //Boundary coordinates of the Library building (clockwise) public static final List LIBRARY_POLYGON = new ArrayList() {{ @@ -36,6 +62,9 @@ public class BuildingPolygon { add(new LatLng(BuildingPolygon.LIBRARY_NE.latitude,BuildingPolygon.LIBRARY_SW.longitude));//(North-West) }}; + + + /** * Function to check if a point is in the Nucleus Building * @param point the point to be checked if inside the building @@ -54,6 +83,14 @@ public static boolean inLibrary(LatLng point){ return (pointInPolygon(point,LIBRARY_POLYGON)); } + public static boolean inFleeming(LatLng point){ + return (pointInPolygon(point, Fleeming_POLYGON)); // Ensure polygon data is correct + } + + public static boolean inHudson(LatLng point){ + return (pointInPolygon(point, Hudson_POLYGON)); // Ensure polygon data is correct + } + /** * Function to check if point in polygon (approximates earth to be flat) * Ray casting algorithm https://en.wikipedia.org/wiki/Point_in_polygon diff --git a/app/src/main/java/com/openpositioning/PositionMe/FusionFilter/EKFFilter.java b/app/src/main/java/com/openpositioning/PositionMe/FusionFilter/EKFFilter.java new file mode 100644 index 00000000..6ef6ae73 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/FusionFilter/EKFFilter.java @@ -0,0 +1,242 @@ +package com.openpositioning.PositionMe.FusionFilter; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.gms.maps.model.LatLng; +import com.openpositioning.PositionMe.PdrProcessing; +import com.openpositioning.PositionMe.sensors.GNSSDataProcessor; +import com.openpositioning.PositionMe.sensors.WiFiPositioning; + +/** + * Extended Kalman Filter (EKF) implementation for multi-sensor fusion positioning + * Fuses WiFi, GNSS and PDR data to achieve more accurate position estimation + */ +public class EKFFilter { + private static final String TAG = "EKFFilter"; + + // Earth-related constants + private static final double EARTH_RADIUS = 6371000.0; // Earth radius in meters + private static final double METERS_PER_DEGREE_LAT = 111320.0; // Meters per degree of latitude + + // Dimension definitions + private static final int STATE_DIMENSION = 2; // State vector dimension [latitude, longitude] + + // Noise parameters + private double wifiNoise; + private double gnssNoise; + private double pdrNoise; + + // State and covariance + private static EKFState ekfState = null; + + // System noise covariance matrix and sensor measurement noise covariance matrices + private static double[][] Q = new double[0][]; + private static double[][] R_wifi = new double[0][]; + private static double[][] R_gnss = new double[0][]; + private static double[][] R_pdr; + + // Identity matrix + private static final double[][] IDENTITY_MATRIX = { + {1, 0}, + {0, 1} + }; + + private static double[][] createDiagonalMatrix(double value) { + return new double[][]{ + {value, 0}, + {0, value} + }; + } + + private static void update(@NonNull LatLng observation, double[][] R_sensor) { + try { + double[] x = ekfState.getState(); + double[][] P = ekfState.getCovariance(); + double[] z = {observation.latitude, observation.longitude}; + double[] y = subtractVector(z, x); + double[][] S = addMatrix(P, R_sensor); + double[][] S_inv = invert2x2(S); + double[][] K = multiplyMatrix(P, S_inv); + double[] K_y = multiplyMatrixVector(K, y); + double[] x_new = addVector(x, K_y); + double[][] I_minus_K = subtractMatrix(IDENTITY_MATRIX, K); + double[][] P_new = multiplyMatrix(I_minus_K, P); + ekfState.setState(x_new); + ekfState.setCovariance(P_new); + Log.d(TAG, String.format("Updated state: [%.6f, %.6f], innovation: [%.6f, %.6f]", x_new[0], x_new[1], y[0], y[1])); + } catch (Exception e) { + Log.e(TAG, "Error in EKF update: " + e.getMessage(), e); + } + } + + public static void updateWiFi(@Nullable LatLng observation) { + if (isValidCoordinate(observation)) { + update(observation, R_wifi); + } + } + + public static void updateGNSS(@Nullable LatLng observation) { + if (isValidCoordinate(observation)) { + update(observation, R_gnss); + } + } + + public void updatePDR(@Nullable LatLng observation) { + if (isValidCoordinate(observation)) { + update(observation, R_pdr); + } + } + + public static LatLng ekfFusion(LatLng initialPosition, LatLng wifiCoord, LatLng gnssCoord, float dx, float dy) { + int wifiNoise = 4; + int gnssNoise = 4; + int pdrNoise = 1; + int initialVariance = 10; + + ekfState = new EKFState(initialPosition, initialVariance); + + Q = createDiagonalMatrix(pdrNoise * pdrNoise); + R_wifi = createDiagonalMatrix(wifiNoise * wifiNoise); + R_gnss = createDiagonalMatrix(gnssNoise * gnssNoise); + R_pdr = createDiagonalMatrix(pdrNoise * pdrNoise); + + // Step 1: Prediction - Advance state using PDR displacement (critical) + predictWithPDR(dx, dy); // This step is very important! + + // Step 2: Update - Correct current predicted position using GNSS/WiFi + if (isValidCoordinate(gnssCoord)) { + updateGNSS(gnssCoord); + } + if (isValidCoordinate(wifiCoord)) { + updateWiFi(wifiCoord); + } + + return getEstimatedPosition(); + } + + private static void predictWithPDR(float dx, float dy) { + double[] x = ekfState.getState(); // Current state [lat, lon] + double[][] P = ekfState.getCovariance(); // Current covariance matrix + + // 1. Convert dx/dy (meters) to latitude/longitude increments (considering latitude's effect on longitude) + double deltaLat = dy / METERS_PER_DEGREE_LAT; + double metersPerDegreeLon = METERS_PER_DEGREE_LAT * Math.cos(Math.toRadians(x[0])); + double deltaLon = dx / metersPerDegreeLon; + + // 2. State prediction + double[] xPred = { + x[0] + deltaLat, + x[1] + deltaLon + }; + + // 3. Covariance prediction P' = P + Q + double[][] P_pred = addMatrix(P, Q); + + // 4. Save updated state and covariance + ekfState.setState(xPred); + ekfState.setCovariance(P_pred); + + // 5. Print debug log + Log.d(TAG, String.format("PDR Predict: dx=%.2f, dy=%.2f -> Δlat=%.6f, Δlon=%.6f, new pos=[%.6f, %.6f]", + dx, dy, deltaLat, deltaLon, xPred[0], xPred[1])); + } + + + + public static LatLng getEstimatedPosition() { + return ekfState.getEstimatedPosition(); + } + + public static boolean isValidCoordinate(@Nullable LatLng coord) { + if (coord == null) return false; + double lat = coord.latitude; + double lon = coord.longitude; + if (Double.isNaN(lat) || Double.isNaN(lon)) return false; + if (Math.abs(lat) < 1e-5 && Math.abs(lon) < 1e-5) return false; + return lat >= -90 && lat <= 90 && lon >= -180 && lon <= 180; + } + + + private static double[][] addMatrix(double[][] A, double[][] B) { + double[][] C = new double[STATE_DIMENSION][STATE_DIMENSION]; + for (int i = 0; i < STATE_DIMENSION; i++) { + for (int j = 0; j < STATE_DIMENSION; j++) { + C[i][j] = A[i][j] + B[i][j]; + } + } + return C; + } + + private static double[][] subtractMatrix(double[][] A, double[][] B) { + double[][] C = new double[STATE_DIMENSION][STATE_DIMENSION]; + for (int i = 0; i < STATE_DIMENSION; i++) { + for (int j = 0; j < STATE_DIMENSION; j++) { + C[i][j] = A[i][j] - B[i][j]; + } + } + return C; + } + + private static double[][] multiplyMatrix(double[][] A, double[][] B) { + double[][] C = new double[STATE_DIMENSION][STATE_DIMENSION]; + for (int i = 0; i < STATE_DIMENSION; i++) { + for (int j = 0; j < STATE_DIMENSION; j++) { + C[i][j] = 0; + for (int k = 0; k < STATE_DIMENSION; k++) { + C[i][j] += A[i][k] * B[k][j]; + } + } + } + return C; + } + + private static double[] multiplyMatrixVector(double[][] A, double[] v) { + double[] result = new double[STATE_DIMENSION]; + for (int i = 0; i < STATE_DIMENSION; i++) { + result[i] = 0; + for (int j = 0; j < STATE_DIMENSION; j++) { + result[i] += A[i][j] * v[j]; + } + } + return result; + } + + private static double[] addVector(double[] a, double[] b) { + double[] c = new double[STATE_DIMENSION]; + for (int i = 0; i < STATE_DIMENSION; i++) { + c[i] = a[i] + b[i]; + } + return c; + } + + private static double[] subtractVector(double[] a, double[] b) { + double[] c = new double[STATE_DIMENSION]; + for (int i = 0; i < STATE_DIMENSION; i++) { + c[i] = a[i] - b[i]; + } + return c; + } + + private static double[][] invert2x2(double[][] M) { + double det = M[0][0] * M[1][1] - M[0][1] * M[1][0]; + if (Math.abs(det) < 1e-10) { + Log.w(TAG, "Matrix is nearly singular, adding regularization term"); + M[0][0] += 1e-8; + M[1][1] += 1e-8; + det = M[0][0] * M[1][1] - M[0][1] * M[1][0]; + if (Math.abs(det) < 1e-10) { + throw new IllegalArgumentException("Matrix is not invertible, determinant too small"); + } + } + double invDet = 1.0 / det; + double[][] inv = new double[2][2]; + inv[0][0] = M[1][1] * invDet; + inv[0][1] = -M[0][1] * invDet; + inv[1][0] = -M[1][0] * invDet; + inv[1][1] = M[0][0] * invDet; + return inv; + } + +} diff --git a/app/src/main/java/com/openpositioning/PositionMe/FusionFilter/EKFState.java b/app/src/main/java/com/openpositioning/PositionMe/FusionFilter/EKFState.java new file mode 100644 index 00000000..69ad3d15 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/FusionFilter/EKFState.java @@ -0,0 +1,194 @@ +package com.openpositioning.PositionMe.FusionFilter; + +import com.google.android.gms.maps.model.LatLng; +import com.openpositioning.PositionMe.sensors.WiFiPositioning; +import org.json.JSONException; +import org.json.JSONObject; + +public class EKFState { + // ------------------------- + // A. Original fields: latitude/longitude + covariance, etc. + // ------------------------- + private static double[] state; // [latitude, longitude] + private double[][] covariance; // 2x2 covariance matrix + + private WiFiPositioning wiFiPositioning; + private LatLng wifiLocation; // WiFi positioning result + + // Latest EKF estimated position (latitude/longitude) + private LatLng EKFPosition; + + // Index variables (can continue to be used as needed) + private int pdrIndex = 0; + private int gnssIndex = 0; + private int pressureIndex = 0; + private int wifiIndex = 0; + + // ------------------------- + // B. New fields: reference point & local plane coordinates + // ------------------------- + // Using initialPosition as the reference latitude/longitude for the local coordinate system + private double baseLat; + private double baseLon; + // Plane coordinates [x, y] (in meters) converted from (lat, lon) relative to the reference point + private float[] localXY; + + /** + * Constructor: Takes an initial position (usually coordinates obtained from WiFi positioning), + * initializes the state vector, covariance matrix, and initiates a WiFi positioning request. + *

+ * Also sets the initial position as the reference point (baseLat, baseLon) for the local coordinate system, + * to automatically maintain localXY afterwards. + * + * @param initialPosition Initial position (LatLng) + * @param initialVariance + */ + public EKFState(LatLng initialPosition, double initialVariance) { + if (initialPosition != null) { + state = new double[]{initialPosition.latitude, initialPosition.longitude}; + EKFPosition = initialPosition; + // Set as local coordinate reference + baseLat = initialPosition.latitude; + baseLon = initialPosition.longitude; + } else { + state = new double[]{0.0, 0.0}; + EKFPosition = new LatLng(0.0, 0.0); + baseLat = 0.0; + baseLon = 0.0; + } + // Initialize covariance matrix as identity matrix + covariance = new double[][] { + {1.0, 0.0}, + {0.0, 1.0} + }; + // Calculate localXY (relative to reference point) + localXY = latLngToXY(state[0], state[1], baseLat, baseLon); + + // Initiate WiFi positioning request to update state + updateWithWiFiPosition(); + } + + /** + * Initiates a WiFi positioning request and updates the state vector and estimated position in the callback + */ + private void updateWithWiFiPosition() { + if (wiFiPositioning == null) { + System.err.println("WiFiPositioning instance not set, cannot initiate WiFi positioning request"); + return; + } + // Construct WiFi fingerprint JSON object, actual data should be generated according to specific business needs + JSONObject wifiFingerprint = new JSONObject(); + try { + // Example: construct a fingerprint containing a single AP data + JSONObject wifiAccessPoints = new JSONObject(); + wifiAccessPoints.put("00:11:22:33:44:55", -45); + wifiFingerprint.put("wf", wifiAccessPoints); + } catch (JSONException e) { + e.printStackTrace(); + } + + // Call WiFi positioning service to get the latest coordinates + wiFiPositioning.request(wifiFingerprint, new WiFiPositioning.VolleyCallback() { + @Override + public void onSuccess(LatLng location, int floor) { + // Update WiFi positioning result and state vector + wifiLocation = location; + state[0] = location.latitude; + state[1] = location.longitude; + EKFPosition = location; + // Update localXY + localXY = latLngToXY(location.latitude, location.longitude, baseLat, baseLon); + System.out.println("WiFi positioning update successful: " + location.latitude + ", " + location.longitude); + } + + @Override + public void onError(String message) { + System.err.println("WiFi positioning error: " + message); + } + }); + } + + /** + * Sets the WiFiPositioning instance used to initiate WiFi positioning requests. + * @param wiFiPositioning WiFi positioning instance + */ + public void setWiFiPositioning(WiFiPositioning wiFiPositioning) { + this.wiFiPositioning = wiFiPositioning; + } + + /** + * Gets the current internal state vector (latitude/longitude). + * @return double array in format [latitude, longitude] + */ + public double[] getState() { + return state; + } + + /** + * Sets a new internal state vector (lat, lon), and updates EKFPosition and localXY + * @param newState New state vector in format [latitude, longitude] + */ + public void setState(double[] newState) { + state = newState; + EKFPosition = new LatLng(newState[0], newState[1]); + // Also update localXY + localXY = latLngToXY(newState[0], newState[1], baseLat, baseLon); + } + + /** + * Gets the current state covariance matrix (2x2). + * @return 2x2 matrix + */ + public double[][] getCovariance() { + return covariance; + } + + /** + * Sets a new state covariance matrix. + * @param newCovariance 2x2 matrix + */ + public void setCovariance(double[][] newCovariance) { + covariance = newCovariance; + } + + /** + * Gets the current EKF estimated position (LatLng object). + * @return Current EKF estimated position + */ + public LatLng getEstimatedPosition() { + return EKFPosition; + } + + + /** + * Converts (lat, lon) to approximate plane coordinates (x, y) relative to (baseLat, baseLon), in meters. + * + * @param lat Current latitude + * @param lon Current longitude + * @param baseLat Reference point latitude + * @param baseLon Reference point longitude + * @return float[] of size 2, (x, y). + */ + private float[] latLngToXY(double lat, double lon, double baseLat, double baseLon) { + double avgLatRad = Math.toRadians((baseLat + lat) / 2.0); + double cosVal = Math.cos(avgLatRad); + + double deltaLon = lon - baseLon; // degrees + double deltaLat = lat - baseLat; // degrees + + // 1 degree of latitude is approximately 111320 meters + // Longitude needs to be multiplied by cos(latitude) + float x = (float) (deltaLon * 111320.0 * cosVal); + float y = (float) (deltaLat * 111320.0); + + return new float[]{ x, y }; + } + + public float[] getEKFPositionAsXY() { + return latLngToXY(EKFPosition.latitude, EKFPosition.longitude, baseLat, baseLon); + } +} + + + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/FusionFilter/ParticleFilter.java b/app/src/main/java/com/openpositioning/PositionMe/FusionFilter/ParticleFilter.java new file mode 100644 index 00000000..fccc92a7 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/FusionFilter/ParticleFilter.java @@ -0,0 +1,244 @@ +package com.openpositioning.PositionMe.FusionFilter; + +import com.google.android.gms.maps.model.LatLng; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +/** + * particle filter for fusing multi-sensor data for position estimation. + * Improvements include: more efficient resampling, motion model optimization, input validation, and code structure refactoring. + */ +public class ParticleFilter { + private List particles; + private final Random random = new Random(); + private final int numParticles; + private final double wifiNoise; + private final double gnssNoise; + private final double pdrNoise; + private LatLng lastValidPosition; + + // Default configuration parameters + private static final int DEFAULT_NUM_PARTICLES = 1000; + private static final double DEFAULT_WIFI_NOISE = 0.000004; + private static final double DEFAULT_GNSS_NOISE = 0.000004; + private static final double DEFAULT_PDR_NOISE = 0.0000065; + + /** + * Full parameter constructor + */ + public ParticleFilter(LatLng initialPosition, int numParticles, + double wifiNoise, double gnssNoise, double pdrNoise) { + if (!isValidCoordinate(initialPosition)) { + throw new IllegalArgumentException("Invalid initial position"); + } + + this.numParticles = numParticles; + this.wifiNoise = wifiNoise; + this.gnssNoise = gnssNoise; + this.pdrNoise = pdrNoise; + this.lastValidPosition = initialPosition; + initializeParticles(initialPosition); + } + + /** + * Simplified constructor (using default parameters) + */ + public ParticleFilter(LatLng initialPosition) { + this(initialPosition, DEFAULT_NUM_PARTICLES, + DEFAULT_WIFI_NOISE, DEFAULT_GNSS_NOISE, DEFAULT_PDR_NOISE); + } + + /** + * Main processing flow + */ + public LatLng particleFilter(LatLng wifiCoord, LatLng gnssCoord, LatLng pdrCoord) { + MotionModel(); + SensorUpdates(wifiCoord, gnssCoord, pdrCoord); + normalizeWeights(); + resample(); + return estimatePosition(); + } + + /** + * Initialize particle swarm + */ + private void initializeParticles(LatLng center) { + particles = new ArrayList<>(numParticles); + final double spread = 0.0001; // Initial spread range + final double halfSpread = spread / 2.0; + final double initWeight = 1.0 / numParticles; + + for (int i = 0; i < numParticles; i++) { + double latOffset = (random.nextDouble() * spread) - halfSpread; + double lonOffset = (random.nextDouble() * spread) - halfSpread; + double lat = center.latitude + latOffset; + double lon = center.longitude + lonOffset; + particles.add(new Particle(lat, lon, initWeight)); + } + } + + + /** + * Motion model (currently simple random walk) + */ + private void MotionModel() { + // Previous fixed implementation + final double movementRange = 0.00001; // Movement perturbation range (example) + final double halfRange = movementRange / 2.0; + + for (Particle p : particles) { + // Generate a random offset in [-halfRange, +halfRange] (uniform distribution) + double baseLatOffset = random.nextDouble() * movementRange - halfRange; + double baseLonOffset = random.nextDouble() * movementRange - halfRange; + + // Simple correction because the actual distance corresponding to each degree of longitude decreases in high latitude areas + double latRad = Math.toRadians(p.position.latitude); + double cosLat = Math.cos(latRad); + + // Avoid extreme case cosLat = 0 (very close to the poles), prevent division by zero + if (Math.abs(cosLat) < 1e-10) { + cosLat = 1e-10 * (cosLat < 0 ? -1 : 1); + } + + // Use the same random amount, keep the original latitude offset. The higher the latitude (smaller cosLat), the better the longitude offset matches the latitude offset on the map + double latOffset = baseLatOffset; + double lonOffset = baseLonOffset / cosLat; + + // Update particle position + p.position = new LatLng( + p.position.latitude + latOffset, + p.position.longitude + lonOffset + ); + } + } + + + + /** + * Sensor data update (multi-source fusion) + */ + private void SensorUpdates(LatLng wifiCoord, LatLng gnssCoord, LatLng pdrCoord) { + if (wifiCoord != null && isValidCoordinate(wifiCoord)) { + updateWeights(wifiCoord, wifiNoise); + } + if (gnssCoord != null && isValidCoordinate(gnssCoord)) { + updateWeights(gnssCoord, gnssNoise); + } + if (pdrCoord != null && isValidCoordinate(pdrCoord)) { + updateWeights(pdrCoord, pdrNoise); + } + } + + /** + * Weight update (optimized distance calculation) + */ + private void updateWeights(LatLng coord, double noise) { + final double denominator = 2 * noise * noise; + + for (Particle p : particles) { + double latDiff = p.position.latitude - coord.latitude; + double lonDiff = p.position.longitude - coord.longitude; + double squaredDistance = latDiff * latDiff + lonDiff * lonDiff; + p.weight *= Math.exp(-squaredDistance / denominator); + } + } + + /** + * Weight normalization (with exception handling) + */ + private void normalizeWeights() { + double total = particles.stream().mapToDouble(p -> p.weight).sum(); + + if (total == 0) { + double uniformWeight = 1.0 / numParticles; + particles.forEach(p -> p.weight = uniformWeight); + } else { + particles.forEach(p -> p.weight /= total); + } + } + + + /** + * Systematic resampling (using binary search to optimize performance) + */ + private void resample() { + int N = numParticles; + double[] cumulativeWeights = new double[N]; + cumulativeWeights[0] = particles.get(0).weight; + for (int i = 1; i < N; i++) { + cumulativeWeights[i] = cumulativeWeights[i - 1] + particles.get(i).weight; + } + + List newParticles = new ArrayList<>(N); + for (int i = 0; i < N; i++) { + double u = random.nextDouble(); + int index = Arrays.binarySearch(cumulativeWeights, u); + // If u is not matched exactly, binarySearch returns -(insertion point) - 1 + if (index < 0) { + index = -index - 1; + } + // Prevent index from going out of bounds + index = Math.min(index, N - 1); + Particle selected = particles.get(index); + newParticles.add(new Particle(selected.position.latitude, + selected.position.longitude, + 1.0 / N)); + } + particles = newParticles; + } + + + /** + * Position estimation (with fault recovery mechanism) + */ + private LatLng estimatePosition() { + if (particles.isEmpty()) { + return lastValidPosition; + } + + double latSum = 0.0; + double lonSum = 0.0; + for (Particle p : particles) { + latSum += p.position.latitude * p.weight; + lonSum += p.position.longitude * p.weight; + } + + // Calculated estimated position + LatLng estimate = new LatLng(latSum, lonSum); + + // If the estimate is valid, update lastValidPosition; otherwise, keep the original value + if (isValidCoordinate(estimate)) { + lastValidPosition = estimate; + } + return lastValidPosition; + } + + + + + /** + * Coordinate validity check + */ + private boolean isValidCoordinate(LatLng coord) { + return coord != null + && !(Double.isNaN(coord.latitude) || Double.isNaN(coord.longitude)) + && Math.abs(coord.latitude) <= 90 + && Math.abs(coord.longitude) <= 180 + && !(coord.latitude == 0 && coord.longitude == 0); + } + + /** + * Particle class (optimized memory layout) + */ + private static class Particle { + LatLng position; + double weight; + + Particle(double lat, double lon, double weight) { + this.position = new LatLng(lat, lon); + this.weight = weight; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/openpositioning/PositionMe/FusionFilter/TrajOptim.java b/app/src/main/java/com/openpositioning/PositionMe/FusionFilter/TrajOptim.java new file mode 100644 index 00000000..e234305a --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/FusionFilter/TrajOptim.java @@ -0,0 +1,67 @@ +package com.openpositioning.PositionMe; + +import com.google.android.gms.maps.model.LatLng; +import java.util.List; + +public class TrajOptim { + + /** + * Apply Weighted Moving Average (WMA) smoothing to a specific index point in the trajectory list + * @param points trajectory point list + * @param windowSize smoothing window size + * @param targetIndex point to smooth (e.g. index = 3) + * @return smoothed LatLng point + */ + public static LatLng applyWMAAtIndex(List points, int windowSize, int targetIndex) { + int size = points.size(); + + if (targetIndex >= size || targetIndex < windowSize - 1) { + return points.get(targetIndex); // Insufficient data, return original point + } + + double sumLat = 0, sumLng = 0; + int weightSum = 0; + + for (int i = 0; i < windowSize; i++) { + int index = targetIndex - i; + int weight = windowSize - i; + sumLat += points.get(index).latitude * weight; + sumLng += points.get(index).longitude * weight; + weightSum += weight; + } + + return new LatLng(sumLat / weightSum, sumLng / weightSum); + } + /** + * Apply low-pass filter to current position (for position smoothing) + * If one of the coordinates is null, return the non-null coordinate; if both are null, return null. + * Also validates alpha range, ensuring it's between 0 and 1. + * + * @param prev Previous frame's coordinate + * @param current Current frame's original coordinate + * @param alpha Smoothing factor (0 ~ 1), recommended 0.1~0.3; will be corrected to valid range if out of bounds + * @return Smoothed coordinate + */ + public static LatLng applyLowPassFilter(LatLng prev, LatLng current, float alpha) { + // Null check: if one is null, return the non-null value; if both are null, return null + if (prev == null && current == null) { + return null; + } else if (prev == null) { + return current; + } else if (current == null) { + return prev; + } + + // Restrict alpha's value range to [0, 1] + if (alpha < 0f) { + alpha = 0f; + } else if (alpha > 1f) { + alpha = 1f; + } + + double lat = prev.latitude * (1 - alpha) + current.latitude * alpha; + double lng = prev.longitude * (1 - alpha) + current.longitude * alpha; + return new LatLng(lat, lng); + } + +} diff --git a/app/src/main/java/com/openpositioning/PositionMe/IndoorMapManager.java b/app/src/main/java/com/openpositioning/PositionMe/IndoorMapManager.java index f9d62a0d..79786c7a 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/IndoorMapManager.java +++ b/app/src/main/java/com/openpositioning/PositionMe/IndoorMapManager.java @@ -1,5 +1,7 @@ package com.openpositioning.PositionMe; + + import android.graphics.Color; import android.util.Log; @@ -23,37 +25,62 @@ * @author Arun Gopalakrishnan */ public class IndoorMapManager { - // To store the map instance + // Google Map instance private GoogleMap gMap; - //Stores the overlay of the indoor maps + // Store current GroundOverlay private GroundOverlay groundOverlay; - // Stores the current Location of user + // Current user location private LatLng currentLocation; - // Stores if indoor map overlay is currently set - private boolean isIndoorMapSet=false; - //Stores the current floor in building + // Whether indoor map is displayed + private boolean isIndoorMapSet = false; + // Current floor private int currentFloor; - // Floor height of current building + // Current building's floor height private float floorHeight; - //Images of the Nucleus Building and Library indoor floor maps - private final List NUCLEUS_MAPS =Arrays.asList( + + // Nucleus Building floor images + private final List NUCLEUS_MAPS = Arrays.asList( R.drawable.nucleuslg, R.drawable.nucleusg, R.drawable.nucleus1, - R.drawable.nucleus2,R.drawable.nucleus3); - private final List LIBRARY_MAPS =Arrays.asList( + R.drawable.nucleus2, R.drawable.nucleus3); + + // Library Building floor images + private final List LIBRARY_MAPS = Arrays.asList( R.drawable.libraryg, R.drawable.library1, R.drawable.library2, R.drawable.library3); - // South-west and north east Bounds of Nucleus building and library to set the Overlay - LatLngBounds NUCLEUS=new LatLngBounds( + + // Fleeming Building floor images + private final List fleeming_MAPS = Arrays.asList( + R.drawable.f0g, R.drawable.f1, R.drawable.f2, + R.drawable.f3); + + private final List Hudson_MAPS = Arrays.asList( + R.drawable.h0g, R.drawable.h1, R.drawable.h2); + + // Nucleus and Library boundaries + private final LatLngBounds NUCLEUS = new LatLngBounds( BuildingPolygon.NUCLEUS_SW, BuildingPolygon.NUCLEUS_NE ); - LatLngBounds LIBRARY=new LatLngBounds( + private final LatLngBounds LIBRARY = new LatLngBounds( BuildingPolygon.LIBRARY_SW, BuildingPolygon.LIBRARY_NE ); - //Average Floor Heights of the Buildings - public static final float NUCLEUS_FLOOR_HEIGHT=4.2F; - public static final float LIBRARY_FLOOR_HEIGHT=3.6F; + + // Fleeming Building boundaries + private final LatLngBounds FLEEMING = new LatLngBounds( + new LatLng(55.9220823, -3.1732186), // ✅ Southwest corner (SW) + new LatLng(55.9225463, -3.1726908) // ✅ Northeast corner (NE) + ); + + + + + // Floor heights for each building + public static final float NUCLEUS_FLOOR_HEIGHT = 4.2F; + public static final float LIBRARY_FLOOR_HEIGHT = 3.6F; + public static final float FLEEMING_FLOOR_HEIGHT = 3.6F; + + public static final float HUDSON_FLOOR_HEIGHT = 3.6F; /** * Constructor to set the map instance @@ -103,10 +130,10 @@ public void setCurrentFloor(int newFloor, boolean autoFloor) { newFloor += 1; } // If within bounds and different from floor map currently being shown - if (newFloor>=0 && newFloor=0 && newFloor=0 && newFloor=0 && newFloor pendingPermissions = new ArrayDeque<>(); + //endregion //region Activity Lifecycle @@ -107,28 +128,85 @@ protected void onCreate(Bundle savedInstanceState) { settings.edit().putBoolean("permanentDeny", false).apply(); //Check Permissions - if(ActivityCompat.checkSelfPermission(this, - Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED || - ActivityCompat.checkSelfPermission(this, - Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED || - ActivityCompat.checkSelfPermission(this, - Manifest.permission.ACCESS_WIFI_STATE) != PackageManager.PERMISSION_GRANTED || - ActivityCompat.checkSelfPermission(this, - Manifest.permission.CHANGE_WIFI_STATE) != PackageManager.PERMISSION_GRANTED || - ActivityCompat.checkSelfPermission(this, - Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED || - ActivityCompat.checkSelfPermission(this, - Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || - ActivityCompat.checkSelfPermission(this, - Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || - ActivityCompat.checkSelfPermission(this, - Manifest.permission.ACTIVITY_RECOGNITION) != PackageManager.PERMISSION_GRANTED){ - askLocationPermissions(); - } + checkPermissionsPhase(); + // Handler for global toasts and popups from other classes this.httpResponseHandler = new Handler(); } + private void checkPermissionsPhase() { + pendingPermissions.clear(); + + // check mandatory permissions + for (String perm : MANDATORY_PERMISSIONS) { + if (!hasPermission(perm)) pendingPermissions.add(perm); + } + + // check optional permissions + for (String perm : OPTIONAL_PERMISSIONS) { + if (!hasPermission(perm)) pendingPermissions.add(perm); + } + + if (!pendingPermissions.isEmpty()) { + requestNextPermission(); + } else { + allPermissionsObtained(); + } + } + + private boolean hasPermission(String permission) { + return ActivityCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED; + } + + private void requestNextPermission() { + if (pendingPermissions.isEmpty()) { + allPermissionsObtained(); + return; + } + + String nextPermission = pendingPermissions.pollFirst(); + ActivityCompat.requestPermissions(this, new String[]{nextPermission}, REQUEST_ID_PERMISSIONS); + } + + private boolean isMandatoryPermission(String permission) { + for (String p : MANDATORY_PERMISSIONS) { + if (p.equals(permission)) return true; + } + return false; + } + + private void handleMandatoryDenied(String deniedPermission) { + if (ActivityCompat.shouldShowRequestPermissionRationale(this, deniedPermission)) { + showRationaleDialog(deniedPermission); + } else { + showPermanentDenyDialog(); + } + } + + private void showRationaleDialog(String permission) { + new AlertDialog.Builder(this) + .setTitle("Permission Required") + .setMessage("This feature requires the " + permission + " permission to work properly") + .setPositiveButton("Retry", (d, w) -> requestNextPermission()) + .setNegativeButton("Exit", (d, w) -> finishAffinity()) + .show(); + } + + private void showPermanentDenyDialog() { + new AlertDialog.Builder(this) + .setTitle("Permissions Required") + .setMessage("You have permanently denied required permissions. Please enable them in settings.") + .setPositiveButton("Settings", (d, w) -> openAppSettings()) + .setNegativeButton("Exit", (d, w) -> finishAffinity()) + .show(); + } + + private void openAppSettings() { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.fromParts("package", getPackageName(), null)); + startActivity(intent); + } + /** * {@inheritDoc} */ @@ -338,81 +416,18 @@ private void askMotionPermissions() { @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); - switch (requestCode) { - case REQUEST_ID_LOCATION_PERMISSION: { // Location permissions - // If request is cancelled results are empty - if (grantResults.length > 1 && - grantResults[0] == PackageManager.PERMISSION_GRANTED && - grantResults[1] == PackageManager.PERMISSION_GRANTED && - grantResults[2] == PackageManager.PERMISSION_GRANTED) { - Toast.makeText(this, "Location permissions granted!", Toast.LENGTH_SHORT).show(); - this.settings.edit().putBoolean("gps", true).apply(); - askWifiPermissions(); - } - else { - if(!settings.getBoolean("permanentDeny", false)) { - permissionsDeniedFirst(); - } - else permissionsDeniedPermanent(); - Toast.makeText(this, "Location permissions denied!", Toast.LENGTH_SHORT).show(); - // Unset setting - this.settings.edit().putBoolean("gps", false).apply(); - } - break; - - } - case REQUEST_ID_WIFI_PERMISSION: { // Wifi permissions - // If request is cancelled results are empty - if (grantResults.length > 1 && - grantResults[0] == PackageManager.PERMISSION_GRANTED && - grantResults[1] == PackageManager.PERMISSION_GRANTED) { - Toast.makeText(this, "Permissions granted!", Toast.LENGTH_SHORT).show(); - this.settings.edit().putBoolean("wifi", true).apply(); - askStoragePermission(); - } - else { - if(!settings.getBoolean("permanentDeny", false)) { - permissionsDeniedFirst(); - } - else permissionsDeniedPermanent(); - Toast.makeText(this, "Wifi permissions denied!", Toast.LENGTH_SHORT).show(); - // Unset setting - this.settings.edit().putBoolean("wifi", false).apply(); - } - break; - } - case REQUEST_ID_READ_WRITE_PERMISSION: { // Read write permissions - // If request is cancelled results are empty - if (grantResults.length > 1 && - grantResults[0] == PackageManager.PERMISSION_GRANTED && - grantResults[1] == PackageManager.PERMISSION_GRANTED) { - Toast.makeText(this, "Permissions granted!", Toast.LENGTH_SHORT).show(); - askMotionPermissions(); - } - else { - if(!settings.getBoolean("permanentDeny", false)) { - permissionsDeniedFirst(); - } - else permissionsDeniedPermanent(); - Toast.makeText(this, "Storage permissions denied!", Toast.LENGTH_SHORT).show(); - } - break; - } - case REQUEST_ID_ACTIVITY_PERMISSION: { // Activity permissions - // If request is cancelled results are empty - if (grantResults.length >= 1 && - grantResults[0] == PackageManager.PERMISSION_GRANTED) { - Toast.makeText(this, "Permissions granted!", Toast.LENGTH_SHORT).show(); - allPermissionsObtained(); - } - else { - if(!settings.getBoolean("permanentDeny", false)) { - permissionsDeniedFirst(); - } - else permissionsDeniedPermanent(); - Toast.makeText(this, "Activity permissions denied!", Toast.LENGTH_SHORT).show(); - } - break; + if (requestCode != REQUEST_ID_PERMISSIONS) return; + + boolean isMandatory = isMandatoryPermission(permissions[0]); + + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + requestNextPermission(); + } else { + if (isMandatory) { + handleMandatoryDenied(permissions[0]); + } else { + // Optional permission denied - just move on + requestNextPermission(); } } } diff --git a/app/src/main/java/com/openpositioning/PositionMe/PdrProcessing.java b/app/src/main/java/com/openpositioning/PositionMe/PdrProcessing.java index bcc7ed10..eaa07da1 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/PdrProcessing.java +++ b/app/src/main/java/com/openpositioning/PositionMe/PdrProcessing.java @@ -146,7 +146,7 @@ public float[] updatePdr(long currentStepEnd, List accelMagnitudeOvertim // return current position, do not update return new float[]{this.positionX, this.positionY}; } - + // Calculate step length if(!useManualStep) { //ArrayList accelMagnitudeFiltered = filter(accelMagnitudeOvertime); diff --git a/app/src/main/java/com/openpositioning/PositionMe/ProgressResponseBody.java b/app/src/main/java/com/openpositioning/PositionMe/ProgressResponseBody.java new file mode 100644 index 00000000..1ccf1fd9 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/ProgressResponseBody.java @@ -0,0 +1,73 @@ +package com.openpositioning.PositionMe; + +import java.io.IOException; + +import okhttp3.MediaType; +import okhttp3.ResponseBody; +import okio.Buffer; +import okio.BufferedSource; +import okio.ForwardingSource; +import okio.Okio; +import okio.Source; + +/** + * A response body wrapper that reports download progress + */ +public class ProgressResponseBody extends ResponseBody { + private final ResponseBody responseBody; + private final ProgressListener progressListener; + private BufferedSource bufferedSource; + + public ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) { + this.responseBody = responseBody; + this.progressListener = progressListener; + } + + @Override + public MediaType contentType() { + return responseBody.contentType(); + } + + @Override + public long contentLength() { + return responseBody.contentLength(); + } + + @Override + public BufferedSource source() { + if (bufferedSource == null) { + bufferedSource = Okio.buffer(source(responseBody.source())); + } + return bufferedSource; + } + + private Source source(Source source) { + return new ForwardingSource(source) { + long totalBytesRead = 0L; + + @Override + public long read(Buffer sink, long byteCount) throws IOException { + long bytesRead = super.read(sink, byteCount); + // Increment the total bytes read + totalBytesRead += bytesRead != -1 ? bytesRead : 0; + + // Calculate progress percentage + long contentLength = responseBody.contentLength(); + float progress = contentLength > 0 ? (100f * totalBytesRead) / contentLength : -1; + + // Report progress to the listener + if (progressListener != null) { + progressListener.update(totalBytesRead, contentLength, bytesRead == -1); + } + return bytesRead; + } + }; + } + + /** + * Interface for reporting download progress + */ + public interface ProgressListener { + void update(long bytesRead, long contentLength, boolean done); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/openpositioning/PositionMe/ServerCommunications.java b/app/src/main/java/com/openpositioning/PositionMe/ServerCommunications.java index f8a1ee3c..e18c95bf 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/ServerCommunications.java +++ b/app/src/main/java/com/openpositioning/PositionMe/ServerCommunications.java @@ -76,7 +76,11 @@ public class ServerCommunications implements Observable { private static final String PROTOCOL_CONTENT_TYPE = "multipart/form-data"; private static final String PROTOCOL_ACCEPT_TYPE = "application/json"; - + // For storing the current download Call object to allow cancellation + private Call currentDownloadCall; + + // Download progress tracking + private int downloadProgress = 0; /** * Public default constructor of {@link ServerCommunications}. The constructor saves context, @@ -272,6 +276,16 @@ public void uploadLocalTrajectory(File localTrajectory) { }); } + /** + * Cancel the current download operation + */ + public void cancelDownload() { + if (currentDownloadCall != null && !currentDownloadCall.isCanceled()) { + currentDownloadCall.cancel(); + System.out.println("Download operation canceled by user"); + } + } + /** * Perform API request for downloading a Trajectory uploaded to the server. The trajectory is * retrieved from a zip file, with the method accepting a position argument specifying the @@ -281,8 +295,31 @@ public void uploadLocalTrajectory(File localTrajectory) { * @param position the position of the trajectory in the zip file to retrieve */ public void downloadTrajectory(int position) { - // Initialise OkHttp client - OkHttpClient client = new OkHttpClient(); + // Reset download progress + downloadProgress = 0; + + // Initialise OkHttp client with progress interceptor + OkHttpClient client = new OkHttpClient.Builder() + .addNetworkInterceptor(chain -> { + okhttp3.Response originalResponse = chain.proceed(chain.request()); + return originalResponse.newBuilder() + .body(new ProgressResponseBody(originalResponse.body(), new ProgressResponseBody.ProgressListener() { + @Override + public void update(long bytesRead, long contentLength, boolean done) { + // Calculate progress percentage (0-100) + int progress = (int) ((100 * bytesRead) / contentLength); + + // Only update UI when progress changes significantly (every 5%) + if (progress > downloadProgress + 5 || done) { + downloadProgress = progress; + infoResponse = "DOWNLOAD_PROGRESS:" + progress; + notifyObservers(0); + } + } + })) + .build(); + }) + .build(); // Create GET request with required header okhttp3.Request request = new okhttp3.Request.Builder() @@ -292,15 +329,32 @@ public void downloadTrajectory(int position) { .build(); // Enqueue the GET request for asynchronous execution - client.newCall(request).enqueue(new okhttp3.Callback() { + currentDownloadCall = client.newCall(request); + currentDownloadCall.enqueue(new okhttp3.Callback() { @Override public void onFailure(Call call, IOException e) { - e.printStackTrace(); + if (call.isCanceled()) { + // User actively canceled the download + infoResponse = "DOWNLOAD_CANCELED"; + } else { + // Other failure reasons + e.printStackTrace(); + infoResponse = "DOWNLOAD_FAILED"; + } + notifyObservers(0); } @Override public void onResponse(Call call, Response response) throws IOException { + // If the request has been canceled, don't process the response + if (call.isCanceled()) { + return; + } + try (ResponseBody responseBody = response.body()) { - if (!response.isSuccessful()) throw new IOException("Unexpected code " - + response); + if (!response.isSuccessful()) { + infoResponse = "DOWNLOAD_FAILED"; + notifyObservers(0); + throw new IOException("Unexpected code " + response); + } // Create input streams to process the response InputStream inputStream = responseBody.byteStream(); @@ -335,31 +389,41 @@ public void downloadTrajectory(int position) { JsonFormat.Printer printer = JsonFormat.printer(); String receivedTrajectoryString = printer.print(receivedTrajectory); System.out.println("Successful download: " - + receivedTrajectoryString.substring(0, 100)); + + (receivedTrajectoryString.length() >= 100 ? + receivedTrajectoryString.substring(0, 100) : + receivedTrajectoryString)); // Save the received trajectory to a file in the Downloads folder //String storagePath = Environment.getExternalStoragePublicDirectory(Environment // .DIRECTORY_DOWNLOADS).toString(); String storagePath = context.getFilesDir().toString(); - - File file = new File(storagePath, "received_trajectory.txt"); - try (FileWriter fileWriter = new FileWriter(file)) { - fileWriter.write(receivedTrajectoryString); - fileWriter.flush(); - System.err.println("Received trajectory stored in: " + storagePath); + + // save the received trajectory as a protobuf file + File file = new File(storagePath, "received_trajectory.traj"); + + try (FileOutputStream fileOutputStream = new FileOutputStream(file)) { + fileOutputStream.write(byteArray); // byteArray is the byte array of the protobuf object + System.err.println("Received trajectory stored in: " + file.getAbsolutePath()); + System.out.println("ReplayFragment PDR number: " + receivedTrajectory.getPdrDataCount()); + System.out.println("ReplayFragment GNSS number: " + receivedTrajectory.getGnssDataCount()); + + // notify the observer that the download is complete + infoResponse = "DOWNLOAD_COMPLETE"; + notifyObservers(0); } catch (IOException ee) { - System.err.println("Trajectory download failed"); - } finally { - // Close all streams and entries to release resources - zipInputStream.closeEntry(); - byteArrayOutputStream.close(); - zipInputStream.close(); - inputStream.close(); + System.err.println("Trajectory download failed: " + ee.getMessage()); + infoResponse = "DOWNLOAD_FAILED"; + notifyObservers(0); } + + // Close all streams and entries to release resources + zipInputStream.closeEntry(); + byteArrayOutputStream.close(); + zipInputStream.close(); + inputStream.close(); } } }); - } /** diff --git a/app/src/main/java/com/openpositioning/PositionMe/fragments/CorrectionFragment.java b/app/src/main/java/com/openpositioning/PositionMe/fragments/CorrectionFragment.java index 2a7c5442..2bde4354 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/fragments/CorrectionFragment.java +++ b/app/src/main/java/com/openpositioning/PositionMe/fragments/CorrectionFragment.java @@ -18,6 +18,9 @@ import androidx.navigation.NavDirections; import androidx.navigation.Navigation; +import com.google.android.gms.maps.model.BitmapDescriptorFactory; +import com.google.android.gms.maps.model.GroundOverlayOptions; +import com.google.android.gms.maps.model.LatLngBounds; import com.openpositioning.PositionMe.PathView; import com.openpositioning.PositionMe.R; import com.openpositioning.PositionMe.sensors.SensorFusion; @@ -77,7 +80,7 @@ public CorrectionFragment() { /** * {@inheritDoc} - * Loads the starting position set in {@link StartLocationFragment}, and displays a map fragment. + * Loads the starting position set in {@link RecordingFragment}, and displays a map fragment. */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -128,6 +131,7 @@ public void onMapReady(GoogleMap map) { System.out.println("onMapReady zoom: " + zoom); //Center the camera mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(start, (float) zoom)); + } }); diff --git a/app/src/main/java/com/openpositioning/PositionMe/fragments/FilesFragment.java b/app/src/main/java/com/openpositioning/PositionMe/fragments/FilesFragment.java index 51b7cb2a..f4400a3e 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/fragments/FilesFragment.java +++ b/app/src/main/java/com/openpositioning/PositionMe/fragments/FilesFragment.java @@ -1,6 +1,7 @@ package com.openpositioning.PositionMe.fragments; import android.app.AlertDialog; +import android.app.Dialog; import android.app.DownloadManager; import android.content.DialogInterface; import android.content.Intent; @@ -10,6 +11,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Button; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -34,6 +36,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; /** * A simple {@link Fragment} subclass. The files fragments displays a list of trajectories already @@ -52,9 +55,14 @@ public class FilesFragment extends Fragment implements Observer { private RecyclerView filesList; private TrajDownloadListAdapter listAdapter; private CardView uploadCard; - + private Button REPLAY; + // Class handling HTTP communication private ServerCommunications serverCommunications; + + // Download progress tracking + private boolean downloadInProgress = false; + private Dialog progressDialog; /** * Default public constructor, empty. @@ -88,37 +96,6 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, return rootView; } - /** - * {@inheritDoc} - * Initialises UI elements, including a navigation card to the {@link UploadFragment} and a - * RecyclerView displaying online trajectories. - * - * @see com.openpositioning.PositionMe.viewitems.TrajDownloadViewHolder the View Holder for the list. - * @see TrajDownloadListAdapter the list adapter for displaying the recycler view. - * @see com.openpositioning.PositionMe.R.layout#item_trajectorycard_view the elements in the list. - */ - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - // Get recyclerview - filesList = view.findViewById(R.id.filesList); - // Get clickable card view - uploadCard = view.findViewById(R.id.uploadCard); - uploadCard.setOnClickListener(new View.OnClickListener() { - /** - * {@inheritDoc} - * Navigates to {@link UploadFragment}. - */ - @Override - public void onClick(View view) { - NavDirections action = FilesFragmentDirections.actionFilesFragmentToUploadFragment(); - Navigation.findNavController(view).navigate(action); - } - }); - // Request list of uploaded trajectories from the server. - serverCommunications.sendInfoRequest(); - } - /** * {@inheritDoc} * Called by {@link ServerCommunications} when the response to the HTTP info request is received. @@ -132,19 +109,122 @@ public void update(Object[] singletonStringList) { String infoString = (String) singletonStringList[0]; // Check if the string is non-null and non-empty before processing if(infoString != null && !infoString.isEmpty()) { - // Process string - List> entryList = processInfoResponse(infoString); // Start a handler to be able to modify UI elements new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { - // Update the RecyclerView with data from the server - updateView(entryList); + // Check if this is a progress update + if (infoString.startsWith("DOWNLOAD_PROGRESS:")) { + try { + // Extract progress percentage + int progress = Integer.parseInt(infoString.split(":")[1]); + // Update progress in dialog + updateProgressDialog(progress); + } catch (Exception e) { + e.printStackTrace(); + } + } + // check if the download is complete + else if (infoString.equals("DOWNLOAD_COMPLETE")) { + if (progressDialog != null && progressDialog.isShowing()) { + progressDialog.dismiss(); + } + showDownloadCompleteDialog(); + downloadInProgress = false; + } else if (infoString.equals("DOWNLOAD_FAILED")) { + // handle the download failed case + if (progressDialog != null && progressDialog.isShowing()) { + progressDialog.dismiss(); + } + showDownloadFailedDialog(); + downloadInProgress = false; + } else if (infoString.equals("DOWNLOAD_CANCELED")) { + // User has already canceled the download, no need to show dialog + if (progressDialog != null && progressDialog.isShowing()) { + progressDialog.dismiss(); + } + downloadInProgress = false; + } else { + // handle the normal server info response + List> entryList = processInfoResponse(infoString); + updateView(entryList); + } } }); } } + /** + * Updates the progress dialog with the current download progress + * + * @param progress download progress percentage (0-100) + */ + private void updateProgressDialog(int progress) { + if (progressDialog != null && progressDialog.isShowing()) { + android.widget.TextView progressMessage = progressDialog.findViewById(R.id.progressMessage); + if (progressMessage != null) { + progressMessage.setText(getString(R.string.downloading) + " " + progress + "%"); + } + } + } + + /** + * Shows a dialog indicating the download has completed + */ + private void showDownloadCompleteDialog() { + new AlertDialog.Builder(getContext()) + .setTitle("File downloaded") + .setMessage("Trajectory downloaded to local storage") + .setPositiveButton(R.string.ok, null) + .setNegativeButton(R.string.show_storage, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + startActivity(new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)); + } + }) + .setIcon(R.drawable.ic_baseline_download_24) + .show(); + } + + /** + * Shows a progress dialog for the download + */ + private void showDownloadProgressDialog() { + progressDialog = new Dialog(getContext()); + progressDialog.setContentView(R.layout.progress_dialog); + progressDialog.setCancelable(true); + + // Initialize with 0% progress + android.widget.TextView progressMessage = progressDialog.findViewById(R.id.progressMessage); + if (progressMessage != null) { + progressMessage.setText(getString(R.string.downloading) + " 0%"); + } + + progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + // Cancel download flag + downloadInProgress = false; + // Cancel background download operation + serverCommunications.cancelDownload(); + } + }); + + progressDialog.show(); + } + + /** + * Shows a dialog indicating the download has failed + */ + private void showDownloadFailedDialog() { + new AlertDialog.Builder(getContext()) + .setTitle("Download Failed") + .setMessage("Failed to download trajectory. Please try again.") + .setPositiveButton(R.string.ok, null) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + } + /** * Parses the info response string from the HTTP communication. * Process the data using the Json library and return the matching Java data structure as a @@ -193,22 +273,44 @@ private void updateView(List> entryList) { filesList.setLayoutManager(manager); filesList.setHasFixedSize(true); listAdapter = new TrajDownloadListAdapter(getActivity(), entryList, position -> { + // Show loading dialog + showDownloadProgressDialog(); + // Set download in progress flag + downloadInProgress = true; // Download the appropriate trajectory instance serverCommunications.downloadTrajectory(position); - // Display a pop-up message to direct the user to the download location if necessary. - new AlertDialog.Builder(getContext()) - .setTitle("File downloaded") - .setMessage("Trajectory downloaded to local storage") - .setPositiveButton(R.string.ok, null) - .setNegativeButton(R.string.show_storage, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - startActivity(new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)); - } - }) - .setIcon(R.drawable.ic_baseline_download_24) - .show(); }); filesList.setAdapter(listAdapter); } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + // Get RecyclerView + filesList = view.findViewById(R.id.filesList); + + // Get upload button + uploadCard = view.findViewById(R.id.uploadCard); + uploadCard.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + NavDirections action = FilesFragmentDirections.actionFilesFragmentToUploadFragment(); + Navigation.findNavController(view).navigate(action); + } + }); + + // Get and initialize REPLAY button + REPLAY = view.findViewById(R.id.replayButton); // Make sure there is this button in fragment_files.xml + REPLAY.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + NavDirections action = FilesFragmentDirections.actionFilesFragmentToReplayFragment(); + Navigation.findNavController(view).navigate(action); + } + }); + + // Request server data + serverCommunications.sendInfoRequest(); + } } \ No newline at end of file diff --git a/app/src/main/java/com/openpositioning/PositionMe/fragments/HomeFragment.java b/app/src/main/java/com/openpositioning/PositionMe/fragments/HomeFragment.java index 27174354..72a2a12c 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/fragments/HomeFragment.java +++ b/app/src/main/java/com/openpositioning/PositionMe/fragments/HomeFragment.java @@ -37,6 +37,8 @@ public class HomeFragment extends Fragment { private Button measurements; private Button files; + + /** * Default empty constructor, unused. */ @@ -95,12 +97,12 @@ public void onClick(View view) { this.start.setOnClickListener(new View.OnClickListener() { /** * {@inheritDoc} - * Navigate to the {@link StartLocationFragment} using AndroidX Jetpack. Hides the - * action bar so the map appears on the full screen. + * Navigate to the {@link RecordingFragment} using AndroidX Jetpack. Hides the + * action bar. */ @Override public void onClick(View view) { - NavDirections action = HomeFragmentDirections.actionHomeFragmentToStartLocationFragment(); + NavDirections action = HomeFragmentDirections.actionHomeFragmentToRecordingFragment(); Navigation.findNavController(view).navigate(action); //Show action bar ((AppCompatActivity)getActivity()).getSupportActionBar().hide(); diff --git a/app/src/main/java/com/openpositioning/PositionMe/fragments/RecordingFragment.java b/app/src/main/java/com/openpositioning/PositionMe/fragments/RecordingFragment.java index 977c2e7d..ace2b83a 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/fragments/RecordingFragment.java +++ b/app/src/main/java/com/openpositioning/PositionMe/fragments/RecordingFragment.java @@ -23,6 +23,7 @@ import android.widget.Spinner; import android.widget.Switch; import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -50,7 +51,9 @@ import com.openpositioning.PositionMe.UtilFunctions; import com.openpositioning.PositionMe.sensors.SensorFusion; import com.openpositioning.PositionMe.sensors.SensorTypes; +import com.openpositioning.PositionMe.TrajOptim; +import java.util.ArrayList; import java.util.List; /** @@ -78,7 +81,7 @@ public class RecordingFragment extends Fragment { private ImageView recIcon; //Loading bar to show time remaining before recording automatically ends private ProgressBar timeRemaining; - //Text views to display distance travelled and elevation since beginning of recording + // Text views to display distance travelled and elevation since beginning of recording private TextView elevation; private TextView distanceTravelled; @@ -123,12 +126,56 @@ public class RecordingFragment extends Fragment { private Switch gnss; // GNSS marker private Marker gnssMarker; + // WiFi marker + private Marker wifiMarker; + // WiFi Switch + private Switch wifiSwitch; // Button used to switch colour private Button switchColor; // Current color of polyline private boolean isRed=true; // Switch used to set auto floor private Switch autoFloor; + // Algorithm switch dropdown + private Spinner algorithmSwitchSpinner; + // Flag to track if recording is in progress + private boolean isRecording = false; + // Flag to track if signal is available for recording + private boolean isSignalAvailable = false; + // TextView to show waiting for signal message + private TextView waitingForSignalText; + + // --- Particle Filter Integration Variables --- + // Particle filter instance for sensor fusion + private com.openpositioning.PositionMe.FusionFilter.ParticleFilter particleFilter; + // Extended Kalman filter instance for sensor fusion + private com.openpositioning.PositionMe.FusionFilter.EKFFilter ekfFilter; + // Polyline for the fused path (blue) + private Polyline fusedPolyline; + // Store the fused position + private LatLng currentFusedPosition; + // Next fused location + private LatLng nextFusedLocation; + // Flag indicating if we're in indoor environment + private boolean isIndoor = false; + // Flag indicating if particle filter is active + private boolean isParticleFilterActive = false; + // Flag indicating if EKF filter is active + private boolean isEKFActive = false; + // Flag indicating if batch optimization is active + private boolean isBatchOptimizationActive = false; + // List to store fused trajectory points + private List fusedTrajectoryPoints = new ArrayList<>(); + // --- End Particle Filter Integration Variables --- + + // Switch for trajectory smoothing + private Switch smoothSwitch; + // Flag to track if smoothing is active + private boolean isSmoothing = false; + // Store the last smoothed position for low-pass filter + private LatLng lastSmoothedPosition = null; + // Alpha value for low-pass filter (0-1) + private final float SMOOTHING_ALPHA = 0.3f; /** * Public Constructor for the class. @@ -142,7 +189,7 @@ public RecordingFragment() { * {@inheritDoc} * Gets an instance of the {@link SensorFusion} class, and initialises the context and settings. * Creates a handler for periodically updating the displayed data. - * + * Starts recording and initializes the start position since navigation now comes directly from HomeFragment. */ @Override public void onCreate(Bundle savedInstanceState) { @@ -151,6 +198,16 @@ public void onCreate(Bundle savedInstanceState) { Context context = getActivity(); this.settings = PreferenceManager.getDefaultSharedPreferences(context); this.refreshDataHandler = new Handler(); + + // Initialize fusion variables + this.isParticleFilterActive = false; + this.isEKFActive = false; + this.isBatchOptimizationActive = false; + this.isIndoor = false; + this.fusedTrajectoryPoints = new ArrayList<>(); + + // Do not initialize recording here - wait for WiFi position if indoors + // Will be started in the start button handler after valid position is confirmed } /** @@ -165,7 +222,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, // Inflate the layout for this fragment ((AppCompatActivity)getActivity()).getSupportActionBar().hide(); getActivity().setTitle("Recording..."); - //Obtain start position set in the startLocation fragment + //Obtain start position that was set in onCreate float[] startPosition = sensorFusion.getGNSSLatitude(true); // Initialize map fragment @@ -207,8 +264,23 @@ public void onMapReady(GoogleMap map) { // Adding polyline to map to plot real-time trajectory PolylineOptions polylineOptions=new PolylineOptions() .color(Color.RED) - .add(currentLocation); + .add(currentLocation) + .width(10f) + .zIndex(10f); polyline = gMap.addPolyline(polylineOptions); + + // Add polyline for fused trajectory (blue) + PolylineOptions fusedPolylineOptions = new PolylineOptions() + .color(Color.BLUE) + .width(8f) + .add(currentLocation) + .visible(isParticleFilterActive || isEKFActive) + .zIndex(20f); + fusedPolyline = gMap.addPolyline(fusedPolylineOptions); + + // Initialize fused position with current location + currentFusedPosition = currentLocation; + // Setting current location to set Ground Overlay for indoor map (if in building) indoorMapManager.setCurrentLocation(currentLocation); //Showing an indication of available indoor maps using PolyLines @@ -251,20 +323,100 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat this.previousPosX = 0f; this.previousPosY = 0f; - // Stop button to save trajectory and move to corrections + // Initialize waiting for signal text + this.waitingForSignalText = getView().findViewById(R.id.waitingForSignalText); + + // Initialize recording icon (red dot) as invisible - only shown when recording + this.recIcon = getView().findViewById(R.id.redDot); + this.recIcon.setVisibility(View.GONE); + + // Initialize particle filter as active by default (set in algorithDropdown) + isParticleFilterActive = true; + + // Start/Stop button to control recording and save trajectory this.stopButton = getView().findViewById(R.id.stopButton); + // Initially set as "Start" and disabled + this.stopButton.setText(getString(R.string.start)); + this.stopButton.setEnabled(false); + this.stopButton.setBackgroundColor(Color.GRAY); + // Show waiting for signal message + this.waitingForSignalText.setVisibility(View.VISIBLE); + this.waitingForSignalText.setText(getString(R.string.waiting_for_signal)); + this.stopButton.setOnClickListener(new View.OnClickListener() { /** * {@inheritDoc} - * OnClick listener for button to go to next fragment. - * When button clicked the PDR recording is stopped and the {@link CorrectionFragment} is loaded. + * OnClick listener for Start/Stop button. + * When clicked in "Start" mode, it begins recording and changes to "Stop". + * When clicked in "Stop" mode, it stops recording and navigates to CorrectionFragment. */ @Override public void onClick(View view) { - if(autoStop != null) autoStop.cancel(); - sensorFusion.stopRecording(); - NavDirections action = RecordingFragmentDirections.actionRecordingFragmentToCorrectionFragment(); - Navigation.findNavController(view).navigate(action); + if (!isRecording) { + // Start recording + isRecording = true; + stopButton.setText(getString(R.string.stop)); + + // Set the start location based on environment (indoor/outdoor) + if (isIndoor) { + // For indoor, use WiFi position if available + LatLng wifiPosition = sensorFusion.getLatLngWifiPositioning(); + if (wifiPosition != null) { + // Convert to float array for SensorFusion + float[] wifiArray = new float[] {(float)wifiPosition.latitude, (float)wifiPosition.longitude}; + sensorFusion.setStartGNSSLatitude(wifiArray); + } else { + // Fallback to GNSS if WiFi not available + float[] gnssPosition = sensorFusion.getGNSSLatitude(false); + sensorFusion.setStartGNSSLatitude(gnssPosition); + } + } else { + // For outdoor, use GNSS position + float[] gnssPosition = sensorFusion.getGNSSLatitude(false); + sensorFusion.setStartGNSSLatitude(gnssPosition); + } + + // Start the actual recording + sensorFusion.startRecording(); + // Display a blinking red dot to show recording is in progress + blinkingRecording(); + // Start the refreshing tasks + if (!settings.getBoolean("split_trajectory", false)) { + refreshDataHandler.post(refreshDataTask); + } else { + // If that time limit has been reached: + long limit = settings.getInt("split_duration", 30) * 60000L; + // Set progress bar + timeRemaining.setMax((int) (limit/1000)); + timeRemaining.setScaleY(3f); + + // Create a CountDownTimer object to adhere to the time limit + autoStop = new CountDownTimer(limit, 1000) { + @Override + public void onTick(long l) { + // increment progress bar + timeRemaining.incrementProgressBy(1); + // Get new position and update UI + updateUIandPosition(); + } + + @Override + public void onFinish() { + // Timer done, move to next fragment automatically - will stop recording + sensorFusion.stopRecording(); + NavDirections action = RecordingFragmentDirections.actionRecordingFragmentToCorrectionFragment(); + Navigation.findNavController(view).navigate(action); + } + }.start(); + } + } else { + // Stop recording + isRecording = false; + if(autoStop != null) autoStop.cancel(); + sensorFusion.stopRecording(); + NavDirections action = RecordingFragmentDirections.actionRecordingFragmentToCorrectionFragment(); + Navigation.findNavController(view).navigate(action); + } } }); @@ -274,21 +426,28 @@ public void onClick(View view) { /** * {@inheritDoc} * OnClick listener for button to go to home fragment. - * When button clicked the PDR recording is stopped and the {@link HomeFragment} is loaded. + * When button clicked the PDR recording is stopped (if recording) and the {@link HomeFragment} is loaded. * The trajectory is not saved. */ @Override public void onClick(View view) { - sensorFusion.stopRecording(); + // Only stop recording if we're actually recording + if (isRecording) { + sensorFusion.stopRecording(); + if(autoStop != null) autoStop.cancel(); + } NavDirections action = RecordingFragmentDirections.actionRecordingFragmentToHomeFragment(); Navigation.findNavController(view).navigate(action); - if(autoStop != null) autoStop.cancel(); } }); // Configuring dropdown for switching map types mapDropdown(); // Setting listener for the switching map types dropdown switchMap(); + // Configuring dropdown for switching positioning algorithms + algorithmDropdown(); + // Setting listener for the switching positioning algorithms dropdown + switchAlgorithm(); // Floor changer Buttons this.floorUpButton=getView().findViewById(R.id.floorUpButton); this.floorDownButton=getView().findViewById(R.id.floorDownButton); @@ -334,7 +493,15 @@ public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { if (isChecked){ // Show GNSS eror float[] location = sensorFusion.getSensorValueMap().get(SensorTypes.GNSSLATLONG); - LatLng gnssLocation = new LatLng(location[0],location[1]); + LatLng gnssLocation = null; + + // Validate location data before creating LatLng + if (location != null && location.length >= 2 && + !Float.isNaN(location[0]) && !Float.isNaN(location[1]) && + !Float.isInfinite(location[0]) && !Float.isInfinite(location[1])) { + gnssLocation = new LatLng(location[0], location[1]); + } + gnssError.setVisibility(View.VISIBLE); gnssError.setText(String.format(getString(R.string.gnss_error)+"%.2fm", UtilFunctions.distanceBetweenPoints(currentLocation,gnssLocation))); @@ -349,6 +516,38 @@ public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { } } }); + + //Obtain the WiFi toggle switch + this.wifiSwitch = getView().findViewById(R.id.wifiSwitch); + this.wifiSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + /** + * {@inheritDoc} + * Listener to set WiFi positioning marker. + */ + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { + if (isChecked) { + // Get WiFi position + LatLng wifiLocation = sensorFusion.getLatLngWifiPositioning(); + // Add WiFi marker with blue color if location is available + if (wifiLocation != null) { + wifiMarker = gMap.addMarker( + new MarkerOptions().title("WiFi position") + .position(wifiLocation) + .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE))); + } else { + // Inform user that WiFi positioning isn't available + wifiSwitch.setChecked(false); + Toast.makeText(getContext(), "WiFi positioning not available", Toast.LENGTH_SHORT).show(); + } + } else { + if (wifiMarker != null) { + wifiMarker.remove(); + } + } + } + }); + // Switch colour button this.switchColor=getView().findViewById(R.id.lineColorButton); this.switchColor.setOnClickListener(new View.OnClickListener() { @@ -375,50 +574,225 @@ public void onClick(View view) { // Display the progress of the recording when a max record length is set this.timeRemaining = getView().findViewById(R.id.timeRemainingBar); - // Display a blinking red dot to show recording is in progress - blinkingRecording(); - - // Check if there is manually set time limit: - if(this.settings.getBoolean("split_trajectory", false)) { - // If that time limit has been reached: - long limit = this.settings.getInt("split_duration", 30) * 60000L; - // Set progress bar - this.timeRemaining.setMax((int) (limit/1000)); - this.timeRemaining.setScaleY(3f); - - // Create a CountDownTimer object to adhere to the time limit - this.autoStop = new CountDownTimer(limit, 1000) { - /** - * {@inheritDoc} - * Increment the progress bar to display progress and remaining time. Update the - * observed PDR values, and animate icons based on the data. - */ - @Override - public void onTick(long l) { - // increment progress bar - timeRemaining.incrementProgressBy(1); - // Get new position and update UI - updateUIandPosition(); + // Check for signal availability periodically + checkSignalAvailability(); + + // Obtain the Smooth toggle switch + this.smoothSwitch = getView().findViewById(R.id.smoothSwitch); + this.smoothSwitch.setChecked(false); // Default off + this.smoothSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + /** + * {@inheritDoc} + * Listener to enable/disable trajectory smoothing. + */ + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { + isSmoothing = isChecked; + if (isChecked) { + // When smoothing is enabled, set the last smoothed position to current position + if ((isParticleFilterActive || isEKFActive) && currentFusedPosition != null) { + lastSmoothedPosition = currentFusedPosition; + } + Toast.makeText(getContext(), "Trajectory smoothing enabled", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(getContext(), "Trajectory smoothing disabled", Toast.LENGTH_SHORT).show(); } + } + }); + + // Remove existing recording start logic and let the Start button handle it + } - /** - * {@inheritDoc} - * Finish recording and move to the correction fragment. - * @see CorrectionFragment - */ - @Override - public void onFinish() { - // Timer done, move to next fragment automatically - will stop recording - sensorFusion.stopRecording(); - NavDirections action = RecordingFragmentDirections.actionRecordingFragmentToCorrectionFragment(); - Navigation.findNavController(view).navigate(action); + /** + * Check for GNSS or WiFi signal availability and update the Start button accordingly + */ + private void checkSignalAvailability() { + Handler signalCheckHandler = new Handler(); + signalCheckHandler.postDelayed(new Runnable() { + @Override + public void run() { + // Check if GNSS or WiFi signals are available + boolean hasGnssSignal = false; + boolean hasWifiSignal = false; + + // Check GNSS availability + if (sensorFusion.getSensorValueMap().containsKey(SensorTypes.GNSSLATLONG)) { + float[] location = sensorFusion.getSensorValueMap().get(SensorTypes.GNSSLATLONG); + if (location != null && location.length >= 2) { + hasGnssSignal = true; + } } - }.start(); - } - else { - // No time limit - use a repeating task to refresh UI. - this.refreshDataHandler.post(refreshDataTask); - } + + // Check if WiFi position not available, try GNSS + LatLng wifiLocation = sensorFusion.getLatLngWifiPositioning(); + if (wifiLocation != null) { + hasWifiSignal = true; + // If this is our first WiFi position, determine we're indoors + if (!isSignalAvailable && !isRecording) { + isIndoor = true; + // Update currentLocation to WiFi position for indoor environment + currentLocation = wifiLocation; + currentFusedPosition = wifiLocation; + + // Update marker position if marker exists + if (orientationMarker != null) { + orientationMarker.setPosition(wifiLocation); + // Move camera to WiFi position + gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(wifiLocation, 19f)); + + // Reset polylines with new start position + List points = new ArrayList<>(); + points.add(wifiLocation); + polyline.setPoints(points); + fusedPolyline.setPoints(points); + } + + // Validate WiFi position before initializing particle filter + if (wifiLocation != null && + !Double.isNaN(wifiLocation.latitude) && !Double.isNaN(wifiLocation.longitude) && + !Double.isInfinite(wifiLocation.latitude) && !Double.isInfinite(wifiLocation.longitude) && + Math.abs(wifiLocation.latitude) <= 90 && Math.abs(wifiLocation.longitude) <= 180 && + !(wifiLocation.latitude == 0 && wifiLocation.longitude == 0)) { + try { + particleFilter = new com.openpositioning.PositionMe.FusionFilter.ParticleFilter(wifiLocation); + Log.d("RecordingFragment", "Indoor environment detected, using WiFi position as start"); + } catch (IllegalArgumentException e) { + Log.e("RecordingFragment", "Failed to initialize particle filter with WiFi position: " + e.getMessage()); + particleFilter = null; + } + } else { + Log.e("RecordingFragment", "Invalid WiFi position for particle filter initialization"); + } + } + } else if (hasGnssSignal && !isSignalAvailable && !isRecording) { + // If no WiFi but GNSS available, we're outdoors + isIndoor = false; + // Initialize particle filter with GNSS position + float[] location = sensorFusion.getSensorValueMap().get(SensorTypes.GNSSLATLONG); + // Validate GNSS location before initializing particle filter + if (location != null && location.length >= 2 && + !Float.isNaN(location[0]) && !Float.isNaN(location[1]) && + !Float.isInfinite(location[0]) && !Float.isInfinite(location[1]) && + Math.abs(location[0]) <= 90 && Math.abs(location[1]) <= 180 && + !(location[0] == 0 && location[1] == 0)) { + try { + LatLng gnssLocation = new LatLng(location[0], location[1]); + particleFilter = new com.openpositioning.PositionMe.FusionFilter.ParticleFilter(gnssLocation); + Log.d("RecordingFragment", "Outdoor environment detected, using GNSS position as start"); + } catch (IllegalArgumentException e) { + Log.e("RecordingFragment", "Failed to initialize particle filter with GNSS position: " + e.getMessage()); + particleFilter = null; + } + } else { + Log.e("RecordingFragment", "Invalid GNSS position for particle filter initialization"); + } + } + + // Update button state based on signal availability + isSignalAvailable = hasGnssSignal || hasWifiSignal; + if (isSignalAvailable && !isRecording) { + // Enable Start button if signal is available and not recording + stopButton.setEnabled(true); + stopButton.setBackgroundColor(Color.BLUE); + waitingForSignalText.setVisibility(View.GONE); + } else if (!isSignalAvailable && !isRecording) { + // Keep button disabled if no signal and update message + stopButton.setEnabled(false); + stopButton.setBackgroundColor(Color.GRAY); + waitingForSignalText.setVisibility(View.VISIBLE); + } + + // Check again after a delay (500ms) + signalCheckHandler.postDelayed(this, 500); + } + }, 500); // Initial delay of 500ms + + // Start periodic position refresh before recording starts + startPreRecordingPositionRefresh(); + } + + /** + * Periodically refreshes the marker and camera position before recording starts + */ + private void startPreRecordingPositionRefresh() { + Handler preRecordingHandler = new Handler(); + preRecordingHandler.postDelayed(new Runnable() { + @Override + public void run() { + // Only update if not recording yet + if (!isRecording) { + // Try to get WiFi position first (for indoor) + LatLng newPosition = sensorFusion.getLatLngWifiPositioning(); + + // If WiFi position not available, try GNSS + if (newPosition == null && sensorFusion.getSensorValueMap().containsKey(SensorTypes.GNSSLATLONG)) { + float[] location = sensorFusion.getSensorValueMap().get(SensorTypes.GNSSLATLONG); + if (location != null && location.length >= 2 && + !Float.isNaN(location[0]) && !Float.isNaN(location[1]) && + !Float.isInfinite(location[0]) && !Float.isInfinite(location[1])) { + newPosition = new LatLng(location[0], location[1]); + } + } + + // Update marker and camera if we have a valid position + if (newPosition != null && gMap != null && orientationMarker != null) { + // Additional validation to prevent invalid coordinates + if (!Double.isNaN(newPosition.latitude) && !Double.isNaN(newPosition.longitude) && + !Double.isInfinite(newPosition.latitude) && !Double.isInfinite(newPosition.longitude) && + Math.abs(newPosition.latitude) <= 90 && Math.abs(newPosition.longitude) <= 180 && + !(newPosition.latitude == 0 && newPosition.longitude == 0)) { + + currentLocation = newPosition; + currentFusedPosition = newPosition; + orientationMarker.setPosition(newPosition); + gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(newPosition, 19f)); + + // Reset polylines with new position + List points = new ArrayList<>(); + points.add(newPosition); + if (polyline != null) { + polyline.setPoints(points); + } + if (fusedPolyline != null) { + fusedPolyline.setPoints(points); + } + + // If particle filter is active but not initialized, try to initialize it + if (isParticleFilterActive && particleFilter == null) { + try { + particleFilter = new com.openpositioning.PositionMe.FusionFilter.ParticleFilter(newPosition); + Log.d("RecordingFragment", "Initialized particle filter with position: " + + newPosition.latitude + ", " + newPosition.longitude); + } catch (IllegalArgumentException e) { + Log.e("RecordingFragment", "Failed to initialize particle filter: " + e.getMessage()); + // Don't disable particle filter, we'll try again next time + } + } + // If EKF filter is active but not initialized, try to initialize it + else if (isEKFActive) { + try { + ekfFilter = new com.openpositioning.PositionMe.FusionFilter.EKFFilter(); + // Initialize EKF with zero motion + com.openpositioning.PositionMe.FusionFilter.EKFFilter.ekfFusion( + newPosition, null, null, 0, 0); + Log.d("RecordingFragment", "Initialized EKF filter with position: " + + newPosition.latitude + ", " + newPosition.longitude); + } catch (Exception e) { + Log.e("RecordingFragment", "Failed to initialize EKF filter: " + e.getMessage()); + // Don't disable EKF filter, we'll try again next time + } + } + } else { + Log.w("RecordingFragment", "Invalid position received: " + + newPosition.latitude + ", " + newPosition.longitude); + } + } + + // Schedule next update in 5 seconds + preRecordingHandler.postDelayed(this, 5000); + } + } + }, 5000); // Initial delay of 5 seconds } /** @@ -470,6 +844,141 @@ public void onNothingSelected(AdapterView parent) { } }); } + + /** + * Creates a dropdown for switching positioning algorithms + */ + private void algorithmDropdown() { + // Creating and Initialising options for Algorithm's Dropdown Menu + algorithmSwitchSpinner = (Spinner) getView().findViewById(R.id.algorithmSwitchSpinner); + // Different Algorithm Types + String[] algorithms = new String[]{"No Fusion", "EKF", "Batch optimisation", "Particle filter"}; + ArrayAdapter adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, algorithms); + // Set the Dropdowns menu adapter + algorithmSwitchSpinner.setAdapter(adapter); + // Set Particle filter as default + algorithmSwitchSpinner.setSelection(3); + isParticleFilterActive = true; + } + + /** + * Spinner listener to change positioning algorithm based on user input + */ + private void switchAlgorithm() { + this.algorithmSwitchSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + // Reset all algorithm flags + boolean wasFiltering = isParticleFilterActive || isEKFActive || isBatchOptimizationActive; + isParticleFilterActive = false; + isEKFActive = false; + isBatchOptimizationActive = false; + + switch (position) { + case 0: + // No Fusion selected + if (fusedPolyline != null) { + fusedPolyline.setVisible(false); + } + Toast.makeText(getContext(), "No Fusion algorithm selected", Toast.LENGTH_SHORT).show(); + break; + case 1: + // EKF selected + if (fusedPolyline != null) { + fusedPolyline.setVisible(true); + } + // Initialize EKF with current position + if (currentLocation != null) { + // Validate currentLocation before initializing + if (!Double.isNaN(currentLocation.latitude) && !Double.isNaN(currentLocation.longitude) && + !Double.isInfinite(currentLocation.latitude) && !Double.isInfinite(currentLocation.longitude) && + Math.abs(currentLocation.latitude) <= 90 && Math.abs(currentLocation.longitude) <= 180) { + + try { + // Initialize EKF with current position + ekfFilter = new com.openpositioning.PositionMe.FusionFilter.EKFFilter(); + // If we were previously using particle filter, continuity is maintained by using + // the last fused position as the initial position for EKF + LatLng initialPos = wasFiltering && currentFusedPosition != null ? + currentFusedPosition : currentLocation; + + // Reset EKF initial state with current position + com.openpositioning.PositionMe.FusionFilter.EKFFilter.ekfFusion( + initialPos, null, null, 0, 0); + + Log.d("RecordingFragment", "Initialized EKF filter with position: " + + initialPos.latitude + ", " + initialPos.longitude); + isEKFActive = true; + } catch (Exception e) { + Log.e("RecordingFragment", "Failed to initialize EKF filter: " + e.getMessage()); + isEKFActive = false; + } + } else { + Log.e("RecordingFragment", "Invalid currentLocation for EKF initialization"); + isEKFActive = false; + } + } + Toast.makeText(getContext(), "EKF algorithm selected", Toast.LENGTH_SHORT).show(); + break; + case 2: + // Batch optimisation selected + if (fusedPolyline != null) { + fusedPolyline.setVisible(false); + } + isBatchOptimizationActive = false; // Not implemented + Toast.makeText(getContext(), "Batch optimisation algorithm selected", Toast.LENGTH_SHORT).show(); + break; + case 3: + // Particle filter selected + if (fusedPolyline != null) { + fusedPolyline.setVisible(true); + } + // Initialize particle filter if not already done + if (currentLocation != null) { + // Validate currentLocation before initializing + if (!Double.isNaN(currentLocation.latitude) && !Double.isNaN(currentLocation.longitude) && + !Double.isInfinite(currentLocation.latitude) && !Double.isInfinite(currentLocation.longitude) && + Math.abs(currentLocation.latitude) <= 90 && Math.abs(currentLocation.longitude) <= 180) { + + try { + // If we were previously using EKF, continuity is maintained by using + // the last fused position as the initial position for particle filter + LatLng initialPos = wasFiltering && currentFusedPosition != null ? + currentFusedPosition : currentLocation; + + particleFilter = new com.openpositioning.PositionMe.FusionFilter.ParticleFilter(initialPos); + Log.d("RecordingFragment", "Initialized particle filter with position: " + + initialPos.latitude + ", " + initialPos.longitude); + isParticleFilterActive = true; + } catch (IllegalArgumentException e) { + Log.e("RecordingFragment", "Failed to initialize particle filter: " + e.getMessage()); + isParticleFilterActive = false; + } + } else { + Log.e("RecordingFragment", "Invalid currentLocation for particle filter initialization"); + isParticleFilterActive = false; + } + } else { + // If we can't initialize now, still mark it active but we'll try again later + isParticleFilterActive = true; + } + Toast.makeText(getContext(), "Particle filter algorithm selected", Toast.LENGTH_SHORT).show(); + break; + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + // Default to No Fusion when nothing selected + isParticleFilterActive = false; + isEKFActive = false; + isBatchOptimizationActive = false; + if (fusedPolyline != null) { + fusedPolyline.setVisible(false); + } + } + }); + } /** * Runnable task used to refresh UI elements with live data. * Has to be run through a Handler object to be able to alter UI elements @@ -507,22 +1016,71 @@ private void updateUIandPosition(){ //Show GNSS marker and error if user enables it if (gnss.isChecked() && gnssMarker!=null){ float[] location = sensorFusion.getSensorValueMap().get(SensorTypes.GNSSLATLONG); - LatLng gnssLocation = new LatLng(location[0],location[1]); + LatLng gnssLocation = null; + + // Validate location data before creating LatLng + if (location != null && location.length >= 2 && + !Float.isNaN(location[0]) && !Float.isNaN(location[1]) && + !Float.isInfinite(location[0]) && !Float.isInfinite(location[1])) { + gnssLocation = new LatLng(location[0], location[1]); + } + gnssError.setVisibility(View.VISIBLE); - gnssError.setText(String.format(getString(R.string.gnss_error)+"%.2fm", - UtilFunctions.distanceBetweenPoints(currentLocation,gnssLocation))); - gnssMarker.setPosition(gnssLocation); + + // Show error based on current location (PDR or fused) + if ((isParticleFilterActive || isEKFActive) && currentFusedPosition != null) { + if (gnssLocation != null && currentFusedPosition != null) { + gnssError.setText(String.format(getString(R.string.gnss_error)+"%.2fm", + UtilFunctions.distanceBetweenPoints(currentFusedPosition, gnssLocation))); + } else { + gnssError.setText(getString(R.string.gnss_error)+"N/A"); + } + } else { + if (gnssLocation != null && currentLocation != null) { + gnssError.setText(String.format(getString(R.string.gnss_error)+"%.2fm", + UtilFunctions.distanceBetweenPoints(currentLocation, gnssLocation))); + } else { + gnssError.setText(getString(R.string.gnss_error)+"N/A"); + } + } + + // Update the marker position only if we have a valid gnssLocation + if (gnssLocation != null) { + gnssMarker.setPosition(gnssLocation); + } + } + + //Show WiFi positioning marker if enabled + if (wifiSwitch.isChecked() && wifiMarker != null) { + LatLng wifiLocation = sensorFusion.getLatLngWifiPositioning(); + if (wifiLocation != null) { + wifiMarker.setPosition(wifiLocation); + } } + // Updates current location of user to show the indoor floor map (if applicable) - indoorMapManager.setCurrentLocation(currentLocation); + if ((isParticleFilterActive || isEKFActive) && currentFusedPosition != null) { + // Use fused position for indoor map when any fusion filter is active + indoorMapManager.setCurrentLocation(currentFusedPosition); + } else { + indoorMapManager.setCurrentLocation(currentLocation); + } + float elevationVal = sensorFusion.getElevation(); // Display buttons to allow user to change floors if indoor map is visible if(indoorMapManager.getIsIndoorMapSet()){ setFloorButtonVisibility(View.VISIBLE); // Auto-floor logic if(autoFloor.isChecked()){ - indoorMapManager.setCurrentFloor((int)(elevationVal/indoorMapManager.getFloorHeight()) - ,true); + // Get floor from WiFi positioning when available, default to calculated floor + int wifiFloor = 0; + if (sensorFusion.getLatLngWifiPositioning() != null) { + wifiFloor = sensorFusion.getWifiFloor(); + indoorMapManager.setCurrentFloor(wifiFloor, true); + } else { + // Fallback to elevation calculation if WiFi positioning not available + indoorMapManager.setCurrentFloor((int)(elevationVal/indoorMapManager.getFloorHeight()), true); + } } }else{ // Hide the buttons and switch used to change floor if indoor map is not visible @@ -552,9 +1110,118 @@ private void plotLines(float[] pdrMoved){ List pointsMoved = polyline.getPoints(); pointsMoved.add(nextLocation); polyline.setPoints(pointsMoved); - // Change current location to new location and zoom there - orientationMarker.setPosition(nextLocation); - gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(nextLocation, (float) 19f)); + + // Apply sensor fusion based on selected algorithm + if (isParticleFilterActive || isEKFActive) { + // Get current WiFi and GNSS positions + LatLng wifiPosition = null; + LatLng gnssPosition = null; + + // Get WiFi position if available and indoors + if (isIndoor) { + wifiPosition = sensorFusion.getLatLngWifiPositioning(); + } + + // Get GNSS position if outdoors or WiFi not available + if (!isIndoor || wifiPosition == null) { + if (sensorFusion.getSensorValueMap().containsKey(SensorTypes.GNSSLATLONG)) { + float[] location = sensorFusion.getSensorValueMap().get(SensorTypes.GNSSLATLONG); + if (location != null && location.length >= 2) { + gnssPosition = new LatLng(location[0], location[1]); + } + } + } + + try { + // Apply filter based on selected algorithm + if (isParticleFilterActive) { + // Initialize particle filter if needed + if (particleFilter == null) { + try { + // Validate current location before initializing + if (!Double.isNaN(currentLocation.latitude) && !Double.isNaN(currentLocation.longitude) && + !Double.isInfinite(currentLocation.latitude) && !Double.isInfinite(currentLocation.longitude) && + Math.abs(currentLocation.latitude) <= 90 && Math.abs(currentLocation.longitude) <= 180 && + !(currentLocation.latitude == 0 && currentLocation.longitude == 0)) { + particleFilter = new com.openpositioning.PositionMe.FusionFilter.ParticleFilter(currentLocation); + Log.d("RecordingFragment", "Initialized particle filter in plotLines"); + } else { + // Skip particle filter processing this iteration + throw new IllegalArgumentException("Invalid position for filter initialization"); + } + } catch (IllegalArgumentException e) { + Log.e("RecordingFragment", "Could not initialize particle filter: " + e.getMessage()); + // Update marker with PDR position and continue + orientationMarker.setPosition(nextLocation); + gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(nextLocation, (float) 19f)); + currentLocation = nextLocation; + return; + } + } + + // Apply particle filter + nextFusedLocation = particleFilter.particleFilter(wifiPosition, gnssPosition, nextLocation); + } else if (isEKFActive) { + // Apply EKF filter + // Note: EKF requires PDR increments rather than absolute position + nextFusedLocation = com.openpositioning.PositionMe.FusionFilter.EKFFilter.ekfFusion( + currentFusedPosition != null ? currentFusedPosition : currentLocation, + wifiPosition, + gnssPosition, + pdrMoved[0], // dx + pdrMoved[1] // dy + ); + } else { + // This should not happen - fallback to PDR position + nextFusedLocation = nextLocation; + } + + // Apply smoothing if enabled + if (isSmoothing && nextFusedLocation != null) { + // Use TrajOptim's low-pass filter for smoothing + nextFusedLocation = TrajOptim.applyLowPassFilter( + lastSmoothedPosition, nextFusedLocation, SMOOTHING_ALPHA); + // Update last smoothed position for next iteration + lastSmoothedPosition = nextFusedLocation; + } + + // Update fused trajectory if we have a valid fusion result + if (nextFusedLocation != null && + !Double.isNaN(nextFusedLocation.latitude) && !Double.isNaN(nextFusedLocation.longitude) && + !Double.isInfinite(nextFusedLocation.latitude) && !Double.isInfinite(nextFusedLocation.longitude)) { + + // Update fused trajectory + List fusedPoints = fusedPolyline.getPoints(); + fusedPoints.add(nextFusedLocation); + fusedPolyline.setPoints(fusedPoints); + + // Store for later use + fusedTrajectoryPoints.add(nextFusedLocation); + + // Update marker position with fused position + orientationMarker.setPosition(nextFusedLocation); + // Move camera to fused position + gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(nextFusedLocation, (float) 19f)); + + // Update current fused position + currentFusedPosition = nextFusedLocation; + } else { + // Invalid fusion result, fall back to PDR + Log.w("RecordingFragment", "Invalid fusion result, falling back to PDR"); + orientationMarker.setPosition(nextLocation); + gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(nextLocation, (float) 19f)); + } + } catch (Exception e) { + // If filter fails, fall back to PDR position + Log.e("RecordingFragment", "Filter error: " + e.getMessage()); + orientationMarker.setPosition(nextLocation); + gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(nextLocation, (float) 19f)); + } + } else { + // No fusion active, use PDR position directly + orientationMarker.setPosition(nextLocation); + gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(nextLocation, (float) 19f)); + } } catch (Exception ex){ Log.e("PlottingPDR","Exception: "+ex); @@ -564,8 +1231,48 @@ private void plotLines(float[] pdrMoved){ else{ //Initialise the starting location float[] location = sensorFusion.getGNSSLatitude(true); - currentLocation=new LatLng(location[0],location[1]); - nextLocation=currentLocation; + // Validate location data before creating LatLng + if (location != null && location.length >= 2 && + !Float.isNaN(location[0]) && !Float.isNaN(location[1]) && + !Float.isInfinite(location[0]) && !Float.isInfinite(location[1]) && + Math.abs(location[0]) <= 90 && Math.abs(location[1]) <= 180) { + + currentLocation = new LatLng(location[0], location[1]); + nextLocation = currentLocation; + currentFusedPosition = currentLocation; + nextFusedLocation = currentLocation; + + // Initialize filters based on currently selected algorithm + if (isParticleFilterActive) { + // Initialize particle filter + if (particleFilter == null) { + try { + // Additional check for 0,0 coordinates which are invalid + if (!(currentLocation.latitude == 0 && currentLocation.longitude == 0)) { + particleFilter = new com.openpositioning.PositionMe.FusionFilter.ParticleFilter(currentLocation); + Log.d("RecordingFragment", "Initialized particle filter with starting location"); + } + } catch (IllegalArgumentException e) { + Log.e("RecordingFragment", "Failed to initialize particle filter: " + e.getMessage()); + } + } + } else if (isEKFActive) { + // Initialize EKF filter + try { + if (!(currentLocation.latitude == 0 && currentLocation.longitude == 0)) { + ekfFilter = new com.openpositioning.PositionMe.FusionFilter.EKFFilter(); + // Initialize EKF with zero motion (dx=0, dy=0) + com.openpositioning.PositionMe.FusionFilter.EKFFilter.ekfFusion( + currentLocation, null, null, 0, 0); + Log.d("RecordingFragment", "Initialized EKF filter with starting location"); + } + } catch (Exception e) { + Log.e("RecordingFragment", "Failed to initialize EKF filter: " + e.getMessage()); + } + } + } else { + Log.e("RecordingFragment", "Invalid GNSS data for starting position"); + } } } @@ -586,6 +1293,10 @@ private void setFloorButtonVisibility(int visibility){ private void blinkingRecording() { //Initialise Image View this.recIcon = getView().findViewById(R.id.redDot); + + // Make the red dot visible + this.recIcon.setVisibility(View.VISIBLE); + //Configure blinking animation Animation blinking_rec = new AlphaAnimation(1, 0); blinking_rec.setDuration(800); diff --git a/app/src/main/java/com/openpositioning/PositionMe/fragments/ReplayFragment.java b/app/src/main/java/com/openpositioning/PositionMe/fragments/ReplayFragment.java new file mode 100644 index 00000000..0a144241 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/fragments/ReplayFragment.java @@ -0,0 +1,1515 @@ +package com.openpositioning.PositionMe.fragments; +import com.google.android.gms.maps.model.BitmapDescriptorFactory; +import com.openpositioning.PositionMe.FusionFilter.ParticleFilter; +import com.openpositioning.PositionMe.Traj; +import com.openpositioning.PositionMe.R; +import com.openpositioning.PositionMe.IndoorMapManager; +import com.openpositioning.PositionMe.PdrProcessing; +import com.openpositioning.PositionMe.sensors.WiFiPositioning; + +import android.app.AlertDialog; +import android.app.DownloadManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Color; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.SeekBar; +import android.widget.Switch; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.Spinner; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.util.Pair; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.google.android.gms.maps.CameraUpdateFactory; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.OnMapReadyCallback; +import com.google.android.gms.maps.SupportMapFragment; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.Marker; +import com.google.android.gms.maps.model.MarkerOptions; +import com.google.android.gms.maps.model.Polyline; +import com.google.android.gms.maps.model.PolylineOptions; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.FileInputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Collections; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import com.google.gson.JsonSyntaxException; +import com.google.gson.Gson; +import com.google.protobuf.ByteString; +import com.openpositioning.PositionMe.UtilFunctions; +import com.openpositioning.PositionMe.sensors.SensorTypes; +import org.json.JSONException; +import org.json.JSONObject; + +public class ReplayFragment extends Fragment implements OnMapReadyCallback { + + private boolean isPlaying = false; + private int progress = 0; + private static final int SEEK_TIME = 10000; // Fast forward/rewind step (milliseconds) + private Handler refreshDataHandler; + private Traj.Trajectory receTraj; + private int pdrNum; + private int gnssNum; + private int PressureNum; + private int wifiNum; + private GoogleMap gMap; + private Polyline polyline; + public IndoorMapManager indoorMapManager; + private Marker positionMarker; + private Marker gnssMarker; + private Marker wifiMarker; + private SeekBar seekBar; + // private List pdrCoordinates = new ArrayList<>(); + // private List gnssCoordinates = new ArrayList<>(); + private int MaxProgress; + private TextView tvProgressTime; // time display + + private float pdrX, pdrY; // current progress PDR data + private float orientation; + private float previousPdrX = 0f; + private float previousPdrY = 0f; + private LatLng currentLocation; // current progress location + private LatLng nextLocation; // next progress location + private LatLng gnssLocation; + private float gnssLati, gnssLong; // current progress GNSS data + private float elevation; // current progress elevation + private int pdrIndex = 0; // current progress PDR index + private int gnssIndex = 0; // current progress GNSS index + private int pressureIndex = 0; // current progress pressure index + private int wifiIndex = 0; // current progress wifi index + private boolean GnssOn = false; + private boolean WifiOn = false; + private PdrProcessing pdrProcessing; + private WiFiPositioning wiFiPositioning; + private LatLng wifiLocation; // wifi positioning result + // Algorithm switch dropdown + private Spinner algorithmSwitchSpinner; + + // --- Particle Filter Integration Variables --- + private ParticleFilter particleFilter; + private Polyline pdrPolyline; // Renamed from polyline for clarity + private Polyline fusedPolyline; // Polyline for the fused path + private List> fusedPositionsList; // Stores fused positions with timestamps + private boolean isIndoor = false; // Flag indicating if trajectory is indoor (based on WiFi data) + private boolean isParticleFilterActive = false; // Flag indicating if particle filter is selected + private LatLng currentFusedPosition; // Holds the latest position from the particle filter + private LatLng initialWifiLocation = null; // Stores the first WiFi location if available + private boolean isMapReady = false; // Flag to check if onMapReady has been called + // --- End Particle Filter Integration Variables --- + + // --- EKF Filter Integration Variables --- + private boolean isEkfFilterActive = false; // Flag indicating if EKF filter is selected + private List> ekfPositionsList; // Stores all EKF positions with timestamps + // --- End EKF Filter Integration Variables --- + + // --- New variables for trajectory optimization --- + private List> pdrPositionsList; // Stores all pre-calculated PDR positions with timestamps + private List> wifiPositionsList; // Stores all pre-calculated WiFi positions with timestamps + private boolean isDataPrepared = false; // Flag indicating if data preparation is complete + private AlertDialog loadingDialog; // Dialog shown while preparing data + // --- End new variables --- + + // --- Smooth trajectory variables --- + private List> smoothedParticleFilterPositionsList; // Stores smoothed particle filter positions + private List> smoothedEkfPositionsList; // Stores smoothed EKF positions + private boolean isSmoothingActive = false; // Flag indicating if trajectory smoothing is enabled + private static final int WMA_WINDOW_SIZE = 5; // Window size for WMA smoothing + // --- End smooth trajectory variables --- + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Context context = getActivity(); + + refreshDataHandler = new Handler(); + pdrProcessing = new PdrProcessing(context); + wiFiPositioning = new WiFiPositioning(context); + particleFilter = null; // Initialize particle filter as null + + // Initialize trajectory lists + pdrPositionsList = new ArrayList<>(); + wifiPositionsList = new ArrayList<>(); + fusedPositionsList = new ArrayList<>(); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_replay, container, false); + requireActivity().setTitle("Replay"); + + // Maps initialization + SupportMapFragment mapFragment = (SupportMapFragment) getChildFragmentManager() + .findFragmentById(R.id.replayMap); + if (mapFragment != null) { + mapFragment.getMapAsync(this); + } + + // UI elements initialization + ImageButton playPauseButton = view.findViewById(R.id.btn_play_pause); + ImageButton rewindButton = view.findViewById(R.id.btn_rewind); + ImageButton forwardButton = view.findViewById(R.id.btn_forward); + Button restartButton = view.findViewById(R.id.btn_restart); + Button goToEndButton = view.findViewById(R.id.btn_go_to_end); + Button exitButton = view.findViewById(R.id.btn_exit); + seekBar = view.findViewById(R.id.seek_bar); + Switch gnssSwitch = view.findViewById(R.id.gnssSwitch); + Switch wifiSwitch = view.findViewById(R.id.wifiSwitch); + Switch smoothSwitch = view.findViewById(R.id.smoothSwitch); // New smooth switch + tvProgressTime = view.findViewById(R.id.tv_progress_time); // bound to time display + algorithmSwitchSpinner = view.findViewById(R.id.algorithmSwitchSpinner); + + // Create algorithm dropdown options + setupAlgorithmDropdown(); + + // play/pause button + playPauseButton.setOnClickListener(v -> { + isPlaying = !isPlaying; + if (isPlaying) { + playPauseButton.setImageResource(R.drawable.ic_baseline_pause_circle_filled_24); + startPlayback(); + } else { + playPauseButton.setImageResource(R.drawable.ic_baseline_play_circle_filled_24_b); + pausePlayback(); + } + }); + + // back 10 seconds + rewindButton.setOnClickListener(v -> { + pausePlayback(); + progress = Math.max(progress - SEEK_TIME, 0); + seekBar.setProgress(progress); + redrawPolyline(progress); + if (isPlaying) { + startPlayback(); + } + // Toast.makeText(getContext(), "Rewind 10 seconds", Toast.LENGTH_SHORT).show(); + }); + + // forward 10 seconds + forwardButton.setOnClickListener(v -> { + pausePlayback(); + progress = Math.min(progress + SEEK_TIME, seekBar.getMax()); + seekBar.setProgress(progress); + redrawPolyline(progress); + if (isPlaying) { + startPlayback(); + } + // Toast.makeText(getContext(), "Forward 10 seconds", Toast.LENGTH_SHORT).show(); + }); + + // restart button + restartButton.setOnClickListener(v -> { + pausePlayback(); + progress = 0; + seekBar.setProgress(progress); + redrawPolyline(progress); + if (isPlaying) { + startPlayback(); + } + // Toast.makeText(getContext(), "Restart button clicked", Toast.LENGTH_SHORT).show(); + }); + + // go to end button + goToEndButton.setOnClickListener(v -> { + progress = seekBar.getMax(); + seekBar.setProgress(progress); + redrawPolyline(progress); + pausePlayback(); + // Toast.makeText(getContext(), "Go to End button clicked", Toast.LENGTH_SHORT).show(); + }); + + // exit button + exitButton.setOnClickListener(v -> { + requireActivity().onBackPressed(); + // Toast.makeText(getContext(), "Exit button clicked", Toast.LENGTH_SHORT).show(); + }); + + // GNSS switch + gnssSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (isChecked) { + GnssOn = true; + gnssLocation = new LatLng(gnssLati, gnssLong); + // Set GNSS marker + gnssMarker=gMap.addMarker( + new MarkerOptions().title("GNSS position") + .position(gnssLocation) + .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE))); + Toast.makeText(getContext(), "GNSS Enabled", Toast.LENGTH_SHORT).show(); + } else { + GnssOn = false; + gnssMarker.remove(); + Toast.makeText(getContext(), "GNSS Disabled", Toast.LENGTH_SHORT).show(); + } + }); + + // WiFi switch + wifiSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (isChecked) { + WifiOn = true; + if (wifiLocation != null) { + // Set WiFi marker + wifiMarker = gMap.addMarker( + new MarkerOptions().title("WiFi position") + .position(wifiLocation) + .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE))); + Toast.makeText(getContext(), "WiFi Positioning Enabled", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(getContext(), "Waiting for WiFi position...", Toast.LENGTH_SHORT).show(); + // Process first WiFi data + if (wifiNum > 0) { + processWifiData(0); + } + } + } else { + WifiOn = false; + if (wifiMarker != null) { + wifiMarker.remove(); + } + Toast.makeText(getContext(), "WiFi Positioning Disabled", Toast.LENGTH_SHORT).show(); + } + }); + + // Smooth switch + smoothSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { + isSmoothingActive = isChecked; + if (isChecked) { + Toast.makeText(getContext(), "Trajectory Smoothing Enabled", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(getContext(), "Trajectory Smoothing Disabled", Toast.LENGTH_SHORT).show(); + } + // Redraw trajectory with/without smoothing + if (isDataPrepared) { + redrawPolyline(progress); + } + }); + + // SeekBar Listener + seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + pausePlayback(); + ReplayFragment.this.progress = progress; + seekBar.setProgress(progress); + redrawPolyline(progress); + updateTimeDisplay(progress); + if (isPlaying) { + startPlayback(); + } + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + // Toast.makeText(getContext(), "SeekBar dragged", Toast.LENGTH_SHORT).show(); + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + // Toast.makeText(getContext(), "SeekBar released", Toast.LENGTH_SHORT).show(); + } + }); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + readTrajectoryData(); + + if (pdrNum == 0) { + new AlertDialog.Builder(getContext()) + .setTitle("PDR data invalid") + .setMessage("No PDR data to replay") + .setNegativeButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + requireActivity().onBackPressed(); + } + }) + .setIcon(R.drawable.ic_baseline_download_24) + .show(); + } else { + // Show loading dialog while preparing trajectory data + AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + builder.setTitle("Preparing Data"); + builder.setMessage("Computing trajectories, please wait..."); + builder.setCancelable(false); + loadingDialog = builder.create(); + loadingDialog.show(); + + // Start data preparation in a background thread + new Thread(() -> { + prepareTrajectoryData(); + + // Update UI on main thread when complete + if (getActivity() != null) { + getActivity().runOnUiThread(() -> { + if (loadingDialog != null && loadingDialog.isShowing()) { + loadingDialog.dismiss(); + } + isDataPrepared = true; + // Initialize map with pre-calculated data if map is already ready + if (isMapReady && gMap != null) { + initializeMapWithTrajectory(); + } + }); + } + }).start(); + } + } + + // Map initialization + @Override + public void onMapReady(@NonNull GoogleMap googleMap) { + Log.d("ReplayFragment", "onMapReady"); + gMap = googleMap; + isMapReady = true; // Map is ready now + indoorMapManager = new IndoorMapManager(googleMap); + + gMap.setMapType(GoogleMap.MAP_TYPE_HYBRID); + gMap.getUiSettings().setCompassEnabled(true); + gMap.getUiSettings().setTiltGesturesEnabled(true); + gMap.getUiSettings().setRotateGesturesEnabled(true); + gMap.getUiSettings().setScrollGesturesEnabled(true); + gMap.getUiSettings().setZoomControlsEnabled(false); + + // If data is already prepared, initialize map with trajectories + // Otherwise, map will be initialized once data is ready + if (isDataPrepared) { + initializeMapWithTrajectory(); + } + } + + /** + * Initializes map with pre-calculated trajectory data + * Sets up polylines, markers, and camera position + */ + private void initializeMapWithTrajectory() { + Log.d("ReplayFragment", "Initializing map with trajectory data"); + + if (pdrNum == 0 || pdrPositionsList.isEmpty()) { + Log.e("ReplayFragment", "Cannot initialize map: No PDR data available"); + return; + } + + // Get initial position - use the first point of the PDR positions list + // which was already initialized with the correct starting point (WiFi if indoor, GNSS if outdoor) + LatLng startLocation = pdrPositionsList.get(0).second; + Log.d("ReplayFragment", "Map initialization using start position: " + startLocation); + + // Set camera position + gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(startLocation, 19f)); + + // Initialize position marker + positionMarker = gMap.addMarker(new MarkerOptions() + .position(startLocation) + .title("Current Position") + .flat(true) + .icon(BitmapDescriptorFactory.fromBitmap( + UtilFunctions.getBitmapFromVector(getContext(), R.drawable.ic_baseline_navigation_24)))); + + // Initialize PDR polyline (Red) + PolylineOptions pdrOptions = new PolylineOptions() + .color(Color.RED) + .width(8f) + .add(startLocation) + .zIndex(1); + pdrPolyline = gMap.addPolyline(pdrOptions); + + // Initialize Fused polyline (Blue) + PolylineOptions fusedOptions = new PolylineOptions() + .color(Color.BLUE) + .width(8f) + .add(startLocation) + .zIndex(2) + .visible(isParticleFilterActive); // Visible if particle filter is active + fusedPolyline = gMap.addPolyline(fusedOptions); + + // Setup indoor map if needed + indoorMapManager.setCurrentLocation(startLocation); + indoorMapManager.setIndicationOfIndoorMap(); + + // Set initial location + currentLocation = startLocation; + currentFusedPosition = startLocation; + + // Display position at progress zero + redrawPolyline(0); + + Log.d("ReplayFragment", "Map initialized with trajectory data"); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + // Stop playback when fragment is destroyed + pausePlayback(); + } + + private void readTrajectoryData() { + try { + // Get file path and read the file + File file = new File(requireContext().getFilesDir(), "received_trajectory.traj"); + FileInputStream fileStream = new FileInputStream(file); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + // Read the file into a byte array + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = fileStream.read(buffer)) != -1) { + byteArrayOutputStream.write(buffer, 0, bytesRead); + } + + // Convert the byte array to a protobuf object + byte[] byteArray = byteArrayOutputStream.toByteArray(); + System.out.println("ReplayFragment File load, size is: " + byteArray.length + " bytes"); + receTraj = Traj.Trajectory.parseFrom(byteArray); + + // Extract trajectory details + pdrNum = receTraj.getPdrDataCount(); + gnssNum = receTraj.getGnssDataCount(); + PressureNum = receTraj.getPressureDataCount(); + wifiNum = receTraj.getWifiDataCount(); + + // Log debug information + Log.d("ReplayFragment", "Trajectory parsed successfully. GNSS points: " + gnssNum); + Log.d("ReplayFragment", "Trajectory parsed successfully. PDR points: " + pdrNum); + Log.d("ReplayFragment", "Trajectory parsed successfully. Pressure points: " + PressureNum); + Log.d("ReplayFragment", "Trajectory parsed successfully. WiFi points: " + wifiNum); + Log.d("ReplayFragment", "Start Timestamp: " + receTraj.getStartTimestamp()); + + // if no PDR record, return + if (pdrNum == 0) { + Log.w("ReplayFragment", "No PDR data to replay"); + return; + } + + // Determine if indoor based on WiFi data presence + isIndoor = (wifiNum > 0); + Log.d("ReplayFragment", isIndoor ? "Indoor trajectory detected." : "Outdoor trajectory detected."); + + // Get max progress + if (receTraj.getPdrData(pdrNum-1).getRelativeTimestamp() > Integer.MAX_VALUE) { + MaxProgress = Integer.MAX_VALUE; + Log.w("ReplayFragment", "Trajectory too long, playback limited to 2^31-1 milliseconds"); + } + else { + MaxProgress = (int)receTraj.getPdrData(pdrNum-1).getRelativeTimestamp(); + Log.d("ReplayFragment", "MaxProgress = "+MaxProgress); + } + seekBar.setMax(MaxProgress); + + // initial current progress data + pdrX = receTraj.getPdrData(0).getX(); + pdrY = receTraj.getPdrData(0).getY(); + if (gnssNum > 0) { + gnssLati = receTraj.getGnssData(0).getLatitude(); + gnssLong = receTraj.getGnssData(0).getLongitude(); + } + else { + gnssLati = 0; + gnssLong = 0; + Log.e("ReplayFragment", "No GNSS data!"); + } + elevation = 0; + + // Process first WiFi data to get initial position if indoor + if (isIndoor) { + processWifiData(0); // Fetch the first WiFi location early + } else { + // For outdoor, initial location is based on GNSS + if (gnssNum > 0) { + currentFusedPosition = new LatLng(gnssLati, gnssLong); + } else { + // Handle case with no GNSS for outdoor (use 0,0 or show error?) + currentFusedPosition = new LatLng(0, 0); + Log.e("ReplayFragment", "Outdoor mode but no GNSS data for initial position!"); + } + } + + // Initialize fused positions list + fusedPositionsList = new ArrayList<>(); + + } catch (IOException | JsonSyntaxException e) { + Log.e("ReplayFragment", "Failed to read trajectory", e); + Toast.makeText(getContext(), "Error: Invalid trajectory file", Toast.LENGTH_LONG).show(); + } + } + + // start playback + private void startPlayback() { + refreshDataHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (isPlaying && progress < MaxProgress) { + progress = Math.min(progress + 200, MaxProgress); + Log.d("ReplayFragment", "current Progress = " + progress); + seekBar.setProgress(progress); + updateUIandPosition(progress); + updateTimeDisplay(progress); + refreshDataHandler.postDelayed(this, 200); + } + } + }, 100); + // Toast.makeText(getContext(), "Playback started", Toast.LENGTH_SHORT).show(); + } + + // pause playback + private void pausePlayback() { + refreshDataHandler.removeCallbacksAndMessages(null); + // Toast.makeText(getContext(), "Playback paused", Toast.LENGTH_SHORT).show(); + } + + static List points = new ArrayList<>(); + + // update UI and position + private void updateUIandPosition(int progress) { + // Make sure map, data, and UI elements are ready + if (!isDataPrepared || gMap == null) { + Log.w("ReplayFragment", "updateUIandPosition skipped: Data or map not ready"); + return; + } + + // Simply redraw the polyline for the current progress + // This leverages our pre-calculated trajectory data + redrawPolyline(progress); + } + + // time display formatting + private void updateTimeDisplay(int progress) { + int seconds = (progress / 1000) % 60; + int minutes = (progress / 1000) / 60; + String time = String.format("%d:%02d", minutes, seconds); + tvProgressTime.setText(time); + } + + // find closest PDR index + private int findClosestPdrIndex(int timestamp, int pdrIndex) { + // make sure index is within bounds + int index = Math.min(Math.max(pdrIndex, 0), pdrNum - 1); + + while ((index < pdrNum - 1) && + (receTraj.getPdrData(index + 1).getRelativeTimestamp() <= timestamp)) { + index++; + } + + // Log.d("ReplayFragment", "Closest PDR index: " + index); + return index; + } + + private int findClosestGnssIndex(int timestamp, int gnssIndex) { + // make sure index is within bounds + int index = Math.min(Math.max(gnssIndex, 0), gnssNum - 1); + + while ((index < gnssNum - 1) && + (receTraj.getGnssData(index + 1).getRelativeTimestamp() <= timestamp)) { + index++; + } + + // Log.d("ReplayFragment", "Closest Gnss index: " + index); + return index; + } + + private int findClosestPressureIndex(int timestamp, int pressureIndex) { + if (PressureNum == 0) { + return 0; // No pressure data + } + + // make sure index is within bounds + int index = Math.min(Math.max(pressureIndex, 0), PressureNum - 1); + + while ((index < PressureNum - 1) && + (receTraj.getPressureData(index + 1).getRelativeTimestamp() <= timestamp)) { + index++; + } + + Log.d("ReplayFragment", "Closest Gnss index: " + index); + return index; + } + + /** + * Process WiFi data from trajectory to get positioning + * @param index The WiFi data index in trajectory + */ + private void processWifiData(int index) { + if (wifiNum <= 0 || index >= wifiNum) { + return; + } + + try { + // Get WiFi data at specific index + Traj.WiFi_Sample wifiSample = receTraj.getWifiData(index); + + // Create JSON WiFi fingerprint + JSONObject wifiAccessPoints = new JSONObject(); + for (int i = 0; i < wifiSample.getMacScansCount(); i++) { + Traj.Mac_Scan macScan = wifiSample.getMacScans(i); + wifiAccessPoints.put(String.valueOf(macScan.getMac()), macScan.getRssi()); + } + + // Create WiFi fingerprint JSON request + JSONObject wifiFingerprint = new JSONObject(); + wifiFingerprint.put("wf", wifiAccessPoints); + + // Send request to WiFi positioning server + wiFiPositioning.request(wifiFingerprint, new WiFiPositioning.VolleyCallback() { + @Override + public void onSuccess(LatLng location, int floor) { + wifiLocation = location; + Log.d("ReplayFragment", "WiFi position updated: " + location.latitude + ", " + location.longitude + ", floor: " + floor); + + // If this is the first WiFi data point for an indoor track, handle initial positioning + if (index == 0 && isIndoor) { + initialWifiLocation = location; + Log.d("ReplayFragment", "Initial WiFi location received: " + location); + // If map is already ready, update the initial setup + if (isMapReady && gMap != null && positionMarker != null && pdrPolyline != null && fusedPolyline != null) { + Log.d("ReplayFragment", "Map is ready, updating initial position to WiFi location."); + currentLocation = initialWifiLocation; + currentFusedPosition = initialWifiLocation; + positionMarker.setPosition(initialWifiLocation); + gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(initialWifiLocation, 19f)); + + // Reset polylines to the new start + List startPointList = new ArrayList<>(); + startPointList.add(initialWifiLocation); + pdrPolyline.setPoints(startPointList); + fusedPolyline.setPoints(startPointList); + + // Re-initialize particle filter with the correct start location + particleFilter = new ParticleFilter(initialWifiLocation); + Log.d("ReplayFragment", "ParticleFilter re-initialized with WiFi start location."); + } + } + + // Update WiFi marker immediately on UI thread if WiFi is enabled + if (WifiOn && getActivity() != null) { + requireActivity().runOnUiThread(() -> { + if (wifiMarker != null) { + wifiMarker.setPosition(wifiLocation); + } else { + // Create marker if enabled but doesn't exist + wifiMarker = gMap.addMarker( + new MarkerOptions().title("WiFi position") + .position(wifiLocation) + .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE))); + } + }); + } + } + + @Override + public void onError(String message) { + Log.e("ReplayFragment", "WiFi positioning error: " + message); + } + }); + + } catch (JSONException e) { + Log.e("ReplayFragment", "Error creating WiFi fingerprint JSON: " + e.getMessage()); + } + } + + /** + * Find closest WiFi data index + * @param timestamp Current timestamp + * @param wifiIndex Current WiFi index + * @return Closest WiFi data index + */ + private int findClosestWifiIndex(int timestamp, int wifiIndex) { + if (wifiNum == 0) { + return 0; // No WiFi data + } + + // make sure index is within bounds + int index = Math.min(Math.max(wifiIndex, 0), wifiNum - 1); + + while ((index < wifiNum - 1) && + (receTraj.getWifiData(index + 1).getRelativeTimestamp() <= timestamp)) { + index++; + } + + return index; + } + + /** + * Prepares all trajectory data in advance to optimize performance + * - Calculates all PDR positions + * - Fetches all WiFi positions + * - Computes all fused positions using the particle filter + * - Computes all positions using the EKF filter + */ + private void prepareTrajectoryData() { + if (pdrNum == 0) { + Log.e("ReplayFragment", "No PDR data to prepare"); + return; + } + + Log.d("ReplayFragment", "Started preparing trajectory data..."); + + // Determine if we need to use WiFi data for indoor positioning + boolean needWifiData = isIndoor && wifiNum > 0; + + // If indoor, get WiFi positions first to determine the starting point + if (needWifiData) { + fetchAllWifiPositions(); + + // Check if WiFi data was successfully fetched + if (!wifiPositionsList.isEmpty()) { + // Use first WiFi position as starting point + initialWifiLocation = wifiPositionsList.get(0).second; + Log.d("ReplayFragment", "Using first WiFi position as starting point: " + initialWifiLocation); + } else { + // Fallback to GNSS if WiFi data failed + if (gnssNum > 0) { + initialWifiLocation = new LatLng(receTraj.getGnssData(0).getLatitude(), receTraj.getGnssData(0).getLongitude()); + Log.d("ReplayFragment", "No WiFi positions available, falling back to GNSS: " + initialWifiLocation); + } else { + initialWifiLocation = new LatLng(0, 0); + Log.w("ReplayFragment", "No valid start position available, using (0,0)"); + } + } + } + + // Determine initial position + LatLng startLocation; + if (isIndoor && initialWifiLocation != null) { + startLocation = initialWifiLocation; + Log.d("ReplayFragment", "Using WiFi location as start location"); + } else if (gnssNum > 0) { + startLocation = new LatLng(receTraj.getGnssData(0).getLatitude(), receTraj.getGnssData(0).getLongitude()); + Log.d("ReplayFragment", "Using GNSS location as start location"); + } else { + startLocation = new LatLng(0, 0); + Log.w("ReplayFragment", "No valid start location available"); + } + + // Initialize Particle Filter with determined start location + particleFilter = new ParticleFilter(startLocation); + + // Initialize EKF positions list + ekfPositionsList = new ArrayList<>(); + + // Initialize smoothed position lists + smoothedParticleFilterPositionsList = new ArrayList<>(); + smoothedEkfPositionsList = new ArrayList<>(); + + // Pre-calculate all PDR positions + calculateAllPdrPositions(startLocation); + + // If not already fetched, get WiFi positions + if (!needWifiData && isIndoor && wifiNum > 0) { + fetchAllWifiPositions(); + } + + // Pre-calculate all fused positions (once WiFi and PDR data are ready) + calculateAllFusedPositions(startLocation); + + // Pre-calculate all EKF positions + calculateAllEkfPositions(startLocation); + + // Apply smoothing to the calculated trajectories + calculateSmoothedTrajectories(); + + Log.d("ReplayFragment", "Finished preparing trajectory data"); + } + + /** + * Calculates all PDR positions and stores them with timestamps + */ + private void calculateAllPdrPositions(LatLng startLocation) { + Log.d("ReplayFragment", "Calculating all PDR positions..."); + + // Add start position + pdrPositionsList.add(new Pair<>(0L, startLocation)); + + LatLng currentPdrLocation = startLocation; + float previousPdrX = receTraj.getPdrData(0).getX(); + float previousPdrY = receTraj.getPdrData(0).getY(); + + // Calculate all PDR positions + for (int i = 1; i < pdrNum; i++) { + float currentPdrX = receTraj.getPdrData(i).getX(); + float currentPdrY = receTraj.getPdrData(i).getY(); + + // Calculate PDR displacement + float[] pdrMoved = {currentPdrX - previousPdrX, currentPdrY - previousPdrY}; + + // Calculate new PDR position + LatLng newPdrLocation = UtilFunctions.calculateNewPos(currentPdrLocation, pdrMoved); + + // Store new position with timestamp + long timestamp = receTraj.getPdrData(i).getRelativeTimestamp(); + pdrPositionsList.add(new Pair<>(timestamp, newPdrLocation)); + + // Update for next iteration + currentPdrLocation = newPdrLocation; + previousPdrX = currentPdrX; + previousPdrY = currentPdrY; + } + + Log.d("ReplayFragment", "Calculated " + pdrPositionsList.size() + " PDR positions"); + } + + /** + * Fetches all WiFi positions from server and stores them with timestamps + */ + private void fetchAllWifiPositions() { + Log.d("ReplayFragment", "Fetching all WiFi positions..."); + + // Clear any existing WiFi positions before fetching new ones + wifiPositionsList.clear(); + + // We'll use a CountDownLatch to make sure all WiFi requests are complete + final CountDownLatch latch = new CountDownLatch(wifiNum); + + for (int i = 0; i < wifiNum; i++) { + final int index = i; + final long timestamp = receTraj.getWifiData(i).getRelativeTimestamp(); + + try { + // Create JSON WiFi fingerprint + Traj.WiFi_Sample wifiSample = receTraj.getWifiData(index); + JSONObject wifiAccessPoints = new JSONObject(); + + for (int j = 0; j < wifiSample.getMacScansCount(); j++) { + Traj.Mac_Scan macScan = wifiSample.getMacScans(j); + wifiAccessPoints.put(String.valueOf(macScan.getMac()), macScan.getRssi()); + } + + // Create WiFi fingerprint JSON request + JSONObject wifiFingerprint = new JSONObject(); + wifiFingerprint.put("wf", wifiAccessPoints); + + // Send request to WiFi positioning server + wiFiPositioning.request(wifiFingerprint, new WiFiPositioning.VolleyCallback() { + @Override + public void onSuccess(LatLng location, int floor) { + synchronized (wifiPositionsList) { + wifiPositionsList.add(new Pair<>(timestamp, location)); + + // Store initial WiFi location if this is the first one + if (index == 0) { + initialWifiLocation = location; + Log.d("ReplayFragment", "Initial WiFi location set: " + location); + } + } + latch.countDown(); + } + + @Override + public void onError(String message) { + Log.e("ReplayFragment", "WiFi positioning error: " + message); + latch.countDown(); + } + }); + + } catch (JSONException e) { + Log.e("ReplayFragment", "Error creating WiFi fingerprint JSON: " + e.getMessage()); + latch.countDown(); + } + } + + try { + // Wait for all WiFi requests to complete (with timeout) + boolean completed = latch.await(30, TimeUnit.SECONDS); + if (completed) { + Log.d("ReplayFragment", "Fetched " + wifiPositionsList.size() + " WiFi positions"); + } else { + Log.w("ReplayFragment", "Timeout waiting for WiFi positions, got " + wifiPositionsList.size()); + } + + // Sort WiFi positions by timestamp + Collections.sort(wifiPositionsList, (a, b) -> Long.compare(a.first, b.first)); + + } catch (InterruptedException e) { + Log.e("ReplayFragment", "Interrupted while waiting for WiFi positions", e); + } + } + + /** + * Calculates all fused positions using the particle filter + */ + private void calculateAllFusedPositions(LatLng startLocation) { + Log.d("ReplayFragment", "Calculating all fused positions..."); + + // Add start position + fusedPositionsList.add(new Pair<>(0L, startLocation)); + + // Reset particle filter + particleFilter = new ParticleFilter(startLocation); + + // Use PDR data timestamps to calculate fusion positions, instead of fixed intervals + // This ensures the fusion trajectory is aligned with the PDR trajectory in time + if (pdrPositionsList.isEmpty()) { + Log.e("ReplayFragment", "No PDR positions available for fusion calculation"); + return; + } + + // Use PDR data timestamps as the basis for fusion calculations + // Skip the first point (already added as start point) + for (int i = 1; i < pdrPositionsList.size(); i++) { + // Get current PDR timestamp and position + long timestamp = pdrPositionsList.get(i).first; + LatLng pdrPosition = pdrPositionsList.get(i).second; + + // Find the WiFi position at corresponding timestamp + LatLng wifiPosition = isIndoor ? findClosestPositionByTime(wifiPositionsList, timestamp) : null; + + // Find the GNSS position at corresponding timestamp + LatLng gnssPosition = null; + if (!isIndoor && gnssNum > 0) { + int gnssIdx = findClosestGnssIndex((int)timestamp, 0); + if (gnssIdx < gnssNum) { + gnssPosition = new LatLng( + receTraj.getGnssData(gnssIdx).getLatitude(), + receTraj.getGnssData(gnssIdx).getLongitude() + ); + } + } + + // Apply particle filter + LatLng fusedPosition = particleFilter.particleFilter(wifiPosition, gnssPosition, pdrPosition); + + // Store fusion position with the same timestamp as PDR + fusedPositionsList.add(new Pair<>(timestamp, fusedPosition)); + + // Log every 100 points for debugging + if (i % 100 == 0) { + Log.d("ReplayFragment", "Calculated fusion position " + i + "/" + pdrPositionsList.size() + + " at time " + timestamp + "ms"); + } + } + + Log.d("ReplayFragment", "Calculated " + fusedPositionsList.size() + " fused positions"); + + // Verify timestamp alignment + verifyTimestampAlignment(); + } + + /** + * Verifies that PDR and fusion position timestamps are properly aligned + * Logs warnings if misalignments are detected + */ + private void verifyTimestampAlignment() { + if (pdrPositionsList.isEmpty() || fusedPositionsList.isEmpty()) { + Log.w("ReplayFragment", "Cannot verify timestamp alignment: Empty position lists"); + return; + } + + // Check that PDR and fusion lists have the same number of entries + if (pdrPositionsList.size() != fusedPositionsList.size()) { + Log.w("ReplayFragment", "Timestamp alignment issue: PDR list size (" + + pdrPositionsList.size() + ") differs from fusion list size (" + + fusedPositionsList.size() + ")"); + } + + // Check a sample of timestamps to verify alignment + int sampleSize = Math.min(10, pdrPositionsList.size() - 1); + for (int i = 1; i <= sampleSize; i++) { + // Get index at regular intervals + int index = i * (pdrPositionsList.size() - 1) / sampleSize; + + if (index < pdrPositionsList.size() && index < fusedPositionsList.size()) { + long pdrTimestamp = pdrPositionsList.get(index).first; + long fusedTimestamp = fusedPositionsList.get(index).first; + + if (pdrTimestamp != fusedTimestamp) { + Log.w("ReplayFragment", "Timestamp mismatch at index " + index + + ": PDR=" + pdrTimestamp + "ms, Fused=" + fusedTimestamp + "ms"); + } + } + } + + // Check WiFi timestamp distribution (if available) + if (!wifiPositionsList.isEmpty()) { + long firstWifiTimestamp = wifiPositionsList.get(0).first; + long lastWifiTimestamp = wifiPositionsList.get(wifiPositionsList.size() - 1).first; + + Log.d("ReplayFragment", "WiFi timestamps range: " + firstWifiTimestamp + + "ms to " + lastWifiTimestamp + "ms (" + + wifiPositionsList.size() + " points)"); + } + + // Log summary + Log.d("ReplayFragment", "Timestamp alignment check complete. PDR positions: " + + pdrPositionsList.size() + ", Fusion positions: " + fusedPositionsList.size()); + } + + /** + * Finds the closest position to the given timestamp from a list of position-timestamp pairs + * Improved with additional logging and error handling + */ + private LatLng findClosestPositionByTime(List> positionsList, long timestamp) { + if (positionsList.isEmpty()) { + return null; + } + + // If before first position, return first + if (timestamp <= positionsList.get(0).first) { + return positionsList.get(0).second; + } + + // If after last position, return last + if (timestamp >= positionsList.get(positionsList.size() - 1).first) { + return positionsList.get(positionsList.size() - 1).second; + } + + // Binary search for closest position + int low = 0; + int high = positionsList.size() - 1; + + while (low <= high) { + int mid = (low + high) / 2; + long midTime = positionsList.get(mid).first; + + if (midTime == timestamp) { + return positionsList.get(mid).second; + } else if (midTime < timestamp) { + low = mid + 1; + } else { + high = mid - 1; + } + } + + // At this point, high < low and timestamp is between positionsList[high] and positionsList[low] + // Return the closer one + if (low >= positionsList.size()) { + return positionsList.get(high).second; + } else if (high < 0) { + return positionsList.get(low).second; + } else { + long diffHigh = Math.abs(timestamp - positionsList.get(high).first); + long diffLow = Math.abs(positionsList.get(low).first - timestamp); + + // Log significant time gaps (more than 5 seconds) + long closerDiff = Math.min(diffHigh, diffLow); + if (closerDiff > 5000) { + Log.w("ReplayFragment", "Large time gap (" + closerDiff + + "ms) when finding position for timestamp " + timestamp + "ms"); + } + + return diffHigh < diffLow ? positionsList.get(high).second : positionsList.get(low).second; + } + } + + private void redrawPolyline(int targetProgress) { + Log.d("ReplayFragment", "redrawPolyline called for progress: " + targetProgress); + + // Ensure map, data, and polylines are ready + if (gMap == null || !isDataPrepared || pdrPolyline == null || fusedPolyline == null) { + Log.w("ReplayFragment", "redrawPolyline skipped: Map or data not ready"); + return; + } + + long startTime = System.currentTimeMillis(); + + // Draw PDR trajectory up to targetProgress + drawTrajectoryUpToProgress(pdrPositionsList, pdrPolyline, targetProgress); + + // Draw fused trajectory if fusion algorithm is active + if (isParticleFilterActive || isEkfFilterActive) { + fusedPolyline.setVisible(true); + + // Choose which positions list to use based on selected algorithm and smoothing setting + List> selectedPositionsList; + + if (isParticleFilterActive) { + selectedPositionsList = isSmoothingActive ? smoothedParticleFilterPositionsList : fusedPositionsList; + } else { // isEkfFilterActive + selectedPositionsList = isSmoothingActive ? smoothedEkfPositionsList : ekfPositionsList; + } + + drawTrajectoryUpToProgress(selectedPositionsList, fusedPolyline, targetProgress); + } else { + fusedPolyline.setVisible(false); + } + + // Update current position marker + LatLng currentPosition; + if ((isParticleFilterActive || isEkfFilterActive)) { + List> selectedPositionsList; + + if (isParticleFilterActive) { + selectedPositionsList = isSmoothingActive ? smoothedParticleFilterPositionsList : fusedPositionsList; + } else { // isEkfFilterActive + selectedPositionsList = isSmoothingActive ? smoothedEkfPositionsList : ekfPositionsList; + } + + if (!selectedPositionsList.isEmpty()) { + currentPosition = findClosestPositionByTime(selectedPositionsList, targetProgress); + currentFusedPosition = currentPosition; + } else if (!pdrPositionsList.isEmpty()) { + currentPosition = findClosestPositionByTime(pdrPositionsList, targetProgress); + } else { + Log.e("ReplayFragment", "No position data available for marker update"); + return; + } + } else if (!pdrPositionsList.isEmpty()) { + currentPosition = findClosestPositionByTime(pdrPositionsList, targetProgress); + } else { + Log.e("ReplayFragment", "No position data available for marker update"); + return; + } + currentLocation = currentPosition; + + if (positionMarker != null && currentPosition != null) { + positionMarker.setPosition(currentPosition); + + // Calculate orientation based on movement direction + calculateAndSetOrientation(targetProgress, positionMarker, isParticleFilterActive || isEkfFilterActive); + + // Move camera to current position + gMap.moveCamera(CameraUpdateFactory.newLatLng(currentPosition)); + } + + // Update indoor map if needed + if (currentPosition != null) { + indoorMapManager.setCurrentLocation(currentPosition); + + // Update elevation/floor if pressure data is available + if (PressureNum > 0 && indoorMapManager.getIsIndoorMapSet()) { + pressureIndex = findClosestPressureIndex(targetProgress, 0); + if (pressureIndex < PressureNum) { + elevation = pdrProcessing.updateElevation(receTraj.getPressureData(pressureIndex).getPressure()); + indoorMapManager.setCurrentFloor((int) (elevation / indoorMapManager.getFloorHeight()), true); + } + } + } + + // Update GNSS marker if active + if (GnssOn && gnssNum > 0) { + gnssIndex = findClosestGnssIndex(targetProgress, 0); + if (gnssIndex < gnssNum) { + gnssLocation = new LatLng( + receTraj.getGnssData(gnssIndex).getLatitude(), + receTraj.getGnssData(gnssIndex).getLongitude() + ); + + if (gnssMarker != null) { + gnssMarker.setPosition(gnssLocation); + } else { + gnssMarker = gMap.addMarker( + new MarkerOptions().title("GNSS position") + .position(gnssLocation) + .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE)) + ); + } + } + } + + // Update WiFi marker if active + if (WifiOn && !wifiPositionsList.isEmpty()) { + LatLng wifiPos = findClosestPositionByTime(wifiPositionsList, targetProgress); + if (wifiPos != null) { + wifiLocation = wifiPos; + + if (wifiMarker != null) { + wifiMarker.setPosition(wifiLocation); + } else { + wifiMarker = gMap.addMarker( + new MarkerOptions().title("WiFi position") + .position(wifiLocation) + .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE)) + ); + } + } + } + + // Log performance metrics for redrawing + long endTime = System.currentTimeMillis(); + long duration = endTime - startTime; + if (duration > 100) { // Only log if it takes more than 100ms + Log.d("ReplayFragment", "redrawPolyline took " + duration + "ms for progress " + targetProgress + "ms"); + } + } + + /** + * Draws a trajectory on a polyline up to the specified progress time + */ + private void drawTrajectoryUpToProgress(List> positionsList, Polyline polyline, int targetProgress) { + if (positionsList.isEmpty()) { + Log.w("ReplayFragment", "Cannot draw trajectory: Empty position list"); + return; + } + + // Create new points list starting with the first position + List points = new ArrayList<>(); + points.add(positionsList.get(0).second); + + // Count of points added to track performance + int pointsAdded = 1; + + // Add all points with timestamp <= targetProgress + for (int i = 1; i < positionsList.size(); i++) { + Pair position = positionsList.get(i); + if (position.first <= targetProgress) { + points.add(position.second); + pointsAdded++; + } else { + break; // Stop once we reach positions beyond target progress + } + } + + // Update the polyline + polyline.setPoints(points); + + // Log trajectory drawing information occasionally (every 5 seconds of playback) + if (targetProgress % 5000 == 0 || targetProgress == MaxProgress) { + Log.d("ReplayFragment", "Drew trajectory with " + pointsAdded + + " points up to " + targetProgress + "ms (" + + (int)(targetProgress/1000) + "s)"); + } + } + + /** + * Calculates and sets the orientation for the position marker based on movement direction + */ + private void calculateAndSetOrientation(int targetProgress, Marker marker, boolean useFusedPath) { + List> positionsList; + + // Select the appropriate list based on active algorithm and smoothing setting + if (useFusedPath) { + if (isParticleFilterActive) { + positionsList = isSmoothingActive ? smoothedParticleFilterPositionsList : fusedPositionsList; + } else { // isEkfFilterActive + positionsList = isSmoothingActive ? smoothedEkfPositionsList : ekfPositionsList; + } + } else { + positionsList = pdrPositionsList; + } + + // Find current position and previous position + LatLng currentPos = findClosestPositionByTime(positionsList, targetProgress); + LatLng prevPos = findClosestPositionByTime(positionsList, Math.max(0, targetProgress - 500)); // 500ms earlier + + if (currentPos != null && prevPos != null) { + // Only calculate orientation if there's actual movement + if (!currentPos.equals(prevPos)) { + double deltaLat = currentPos.latitude - prevPos.latitude; + double deltaLng = currentPos.longitude - prevPos.longitude; + + if (Math.abs(deltaLat) > 1e-9 || Math.abs(deltaLng) > 1e-9) { + // Calculate bearing (orientation) + float bearing = (float) Math.toDegrees(Math.atan2(deltaLng, deltaLat)); + marker.setRotation(bearing); + } + } + } + } + + /** + * Calculates all EKF positions and stores them with timestamps + */ + private void calculateAllEkfPositions(LatLng startLocation) { + Log.d("ReplayFragment", "Calculating all EKF positions..."); + + // Add start position + ekfPositionsList.add(new Pair<>(0L, startLocation)); + + // Initialize variables to track PDR increments + float previousPdrX = receTraj.getPdrData(0).getX(); + float previousPdrY = receTraj.getPdrData(0).getY(); + LatLng lastEkfPosition = startLocation; + + // Use PDR data timestamps as the basis for EKF calculations + // Skip the first point (already added as start point) + for (int i = 1; i < pdrNum; i++) { + // Get current PDR data + float currentPdrX = receTraj.getPdrData(i).getX(); + float currentPdrY = receTraj.getPdrData(i).getY(); + + // Calculate PDR displacement (increments) + float dx = currentPdrX - previousPdrX; + float dy = currentPdrY - previousPdrY; + + // Get current timestamp + long timestamp = receTraj.getPdrData(i).getRelativeTimestamp(); + + // Find the WiFi position at corresponding timestamp + LatLng wifiPosition = isIndoor ? findClosestPositionByTime(wifiPositionsList, timestamp) : null; + + // Find the GNSS position at corresponding timestamp + LatLng gnssPosition = null; + if (!isIndoor && gnssNum > 0) { + int gnssIdx = findClosestGnssIndex((int)timestamp, 0); + if (gnssIdx < gnssNum) { + gnssPosition = new LatLng( + receTraj.getGnssData(gnssIdx).getLatitude(), + receTraj.getGnssData(gnssIdx).getLongitude() + ); + } + } + + // Apply EKF filter with the new interface (using dx, dy increments) + LatLng ekfPosition = com.openpositioning.PositionMe.FusionFilter.EKFFilter.ekfFusion( + lastEkfPosition, wifiPosition, gnssPosition, dx, dy); + + // Store EKF position with the same timestamp as PDR + ekfPositionsList.add(new Pair<>(timestamp, ekfPosition)); + + // Update for next iteration + lastEkfPosition = ekfPosition; + previousPdrX = currentPdrX; + previousPdrY = currentPdrY; + + // Log every 100 points for debugging + if (i % 100 == 0) { + Log.d("ReplayFragment", "Calculated EKF position " + i + "/" + pdrNum + + " at time " + timestamp + "ms with dx=" + dx + ", dy=" + dy); + } + } + + Log.d("ReplayFragment", "Calculated " + ekfPositionsList.size() + " EKF positions"); + } + + /** + * Calculates smoothed trajectories using WMA algorithm + */ + private void calculateSmoothedTrajectories() { + Log.d("ReplayFragment", "Calculating smoothed trajectories using WMA..."); + + // Create temporary lists for trajectory points without timestamps + List particleFilterPoints = new ArrayList<>(); + List ekfPoints = new ArrayList<>(); + + // Extract position data from the paired lists + for (Pair position : fusedPositionsList) { + particleFilterPoints.add(position.second); + } + + for (Pair position : ekfPositionsList) { + ekfPoints.add(position.second); + } + + // Apply WMA smoothing for Particle Filter points + smoothedParticleFilterPositionsList.clear(); + for (int i = 0; i < fusedPositionsList.size(); i++) { + long timestamp = fusedPositionsList.get(i).first; + LatLng smoothedPoint; + + if (i < WMA_WINDOW_SIZE - 1) { + // Not enough previous points for full window size, use original point + smoothedPoint = particleFilterPoints.get(i); + } else { + // Apply WMA smoothing with specified window size + smoothedPoint = com.openpositioning.PositionMe.TrajOptim.applyWMAAtIndex(particleFilterPoints, WMA_WINDOW_SIZE, i); + } + + // Add smoothed point with original timestamp + smoothedParticleFilterPositionsList.add(new Pair<>(timestamp, smoothedPoint)); + } + + // Apply WMA smoothing for EKF points + smoothedEkfPositionsList.clear(); + for (int i = 0; i < ekfPositionsList.size(); i++) { + long timestamp = ekfPositionsList.get(i).first; + LatLng smoothedPoint; + + if (i < WMA_WINDOW_SIZE - 1) { + // Not enough previous points for full window size, use original point + smoothedPoint = ekfPoints.get(i); + } else { + // Apply WMA smoothing with specified window size + smoothedPoint = com.openpositioning.PositionMe.TrajOptim.applyWMAAtIndex(ekfPoints, WMA_WINDOW_SIZE, i); + } + + // Add smoothed point with original timestamp + smoothedEkfPositionsList.add(new Pair<>(timestamp, smoothedPoint)); + } + + Log.d("ReplayFragment", "Calculated " + smoothedParticleFilterPositionsList.size() + " smoothed particle filter positions"); + Log.d("ReplayFragment", "Calculated " + smoothedEkfPositionsList.size() + " smoothed EKF positions"); + } + + /** + * Creates a dropdown for switching positioning algorithms + */ + private void setupAlgorithmDropdown() { + // Different Algorithm Types + String[] algorithms = new String[]{"No Fusion", "EKF", "Batch optimisation", "Particle filter"}; + ArrayAdapter adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, algorithms); + // Set the Dropdowns menu adapter + algorithmSwitchSpinner.setAdapter(adapter); + + // Set the default algorithm to Particle filter + algorithmSwitchSpinner.setSelection(3); + isParticleFilterActive = true; // Default to particle filter active + isEkfFilterActive = false; // Default EKF to inactive + + // Set listener for algorithm selection + algorithmSwitchSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + boolean prevParticleFilterActive = isParticleFilterActive; + boolean prevEkfFilterActive = isEkfFilterActive; + + // Reset all algorithm flags first + isParticleFilterActive = false; + isEkfFilterActive = false; + + switch (position) { + case 0: + // No Fusion selected + Toast.makeText(getContext(), "No Fusion algorithm selected", Toast.LENGTH_SHORT).show(); + break; + case 1: + // EKF selected + isEkfFilterActive = true; + Toast.makeText(getContext(), "EKF algorithm selected", Toast.LENGTH_SHORT).show(); + break; + case 2: + // Batch optimisation selected + Toast.makeText(getContext(), "Batch optimisation algorithm selected", Toast.LENGTH_SHORT).show(); + break; + case 3: + // Particle filter selected + isParticleFilterActive = true; + Toast.makeText(getContext(), "Particle filter algorithm selected", Toast.LENGTH_SHORT).show(); + break; + default: + break; + } + + // If filter status changed and data is ready, redraw the current progress + if ((prevParticleFilterActive != isParticleFilterActive || + prevEkfFilterActive != isEkfFilterActive) && isDataPrepared) { + redrawPolyline(progress); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + // Default to No Fusion when nothing selected + isParticleFilterActive = false; + isEkfFilterActive = false; + if (isDataPrepared) { + redrawPolyline(progress); + } + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/openpositioning/PositionMe/fragments/StartLocationFragment.java b/app/src/main/java/com/openpositioning/PositionMe/fragments/StartLocationFragment.java deleted file mode 100644 index 84eb7d93..00000000 --- a/app/src/main/java/com/openpositioning/PositionMe/fragments/StartLocationFragment.java +++ /dev/null @@ -1,202 +0,0 @@ -package com.openpositioning.PositionMe.fragments; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.fragment.app.Fragment; -import androidx.navigation.NavDirections; -import androidx.navigation.Navigation; - -import com.google.android.gms.maps.CameraUpdateFactory; -import com.google.android.gms.maps.GoogleMap; -import com.google.android.gms.maps.OnMapReadyCallback; -import com.google.android.gms.maps.SupportMapFragment; -import com.google.android.gms.maps.model.LatLng; -import com.google.android.gms.maps.model.Marker; -import com.google.android.gms.maps.model.MarkerOptions; -import com.openpositioning.PositionMe.R; -import com.openpositioning.PositionMe.sensors.SensorFusion; - -/** - * A simple {@link Fragment} subclass. The startLocation fragment is displayed before the trajectory - * recording starts. This fragment displays a map in which the user can adjust their location to - * correct the PDR when it is complete - * - * @see HomeFragment the previous fragment in the nav graph. - * @see RecordingFragment the next fragment in the nav graph. - * @see SensorFusion the class containing sensors and recording. - * - * @author Virginia Cangelosi - */ -public class StartLocationFragment extends Fragment { - - //Button to go to next fragment and save the location - private Button button; - //Singleton SesnorFusion class which stores data from all sensors - private SensorFusion sensorFusion = SensorFusion.getInstance(); - //Google maps LatLong object to pass location to the map - private LatLng position; - //Start position of the user to be stored - private float[] startPosition = new float[2]; - //Zoom of google maps - private NucleusBuildingManager NucleusBuildingManager; - private float zoom = 19f; - - /** - * Public Constructor for the class. - * Left empty as not required - */ - public StartLocationFragment() { - // Required empty public constructor - } - - /** - * {@inheritDoc} - * The map is loaded and configured so that it displays a draggable marker for the start location - */ - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - // Inflate the layout for this fragment - ((AppCompatActivity)getActivity()).getSupportActionBar().hide(); - View rootView = inflater.inflate(R.layout.fragment_startlocation, container, false); - - //Obtain the start position from the GPS data from the SensorFusion class - startPosition = sensorFusion.getGNSSLatitude(false); - //If not location found zoom the map out - if(startPosition[0]==0 && startPosition[1]==0){ - zoom = 1f; - } - else { - zoom = 19f; - } - // Initialize map fragment - SupportMapFragment supportMapFragment=(SupportMapFragment) - getChildFragmentManager().findFragmentById(R.id.startMap); - - - // This is just a demonstration of the automatic expansion of the indoor map. - // Assume that we have obtained the user's position "newPosition" from the callback function. >>> - if (newPosition != null) { - // Check if the user's position is inside the defined building polygon - if (NucleusBuildingManager.isPointInBuilding(newPosition)) { - FloorButtons.setVisibility(View.VISIBLE); - switchFloorNU(floor); - InNu = 1; // Mark indoor map status - } else { - NucleusBuildingManager.getIndoorMapManager().hideMap(); - FloorButtons.setVisibility(View.GONE); - InNu = 0; // Mark indoor map status - } - } - - - // Asynchronous map which can be configured - supportMapFragment.getMapAsync(new OnMapReadyCallback() { - /** - * {@inheritDoc} - * Controls to allow scrolling, tilting, rotating and a compass view of the - * map are enabled. A marker is added to the map with the start position and a marker - * drag listener is generated to detect when the marker has moved to obtain the new - * location. - */ - @Override - public void onMapReady(GoogleMap mMap) { - mMap.setMapType(GoogleMap.MAP_TYPE_HYBRID); - mMap.getUiSettings().setCompassEnabled(true); - mMap.getUiSettings().setTiltGesturesEnabled(true); - mMap.getUiSettings().setRotateGesturesEnabled(true); - mMap.getUiSettings().setScrollGesturesEnabled(true); - - - if (mMap != null) { - // Create NuclearBuildingManager instance - NucleusBuildingManager = new NucleusBuildingManager(mMap); - NucleusBuildingManager.getIndoorMapManager().hideMap(); - } - - // Add a marker in current GPS location and move the camera - position = new LatLng(startPosition[0], startPosition[1]); - mMap.addMarker(new MarkerOptions().position(position).title("Start Position")).setDraggable(true); - mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(position, zoom )); - - //Drag listener for the marker to execute when the markers location is changed - mMap.setOnMarkerDragListener(new GoogleMap.OnMarkerDragListener() - { - /** - * {@inheritDoc} - */ - @Override - public void onMarkerDragStart(Marker marker){} - - /** - * {@inheritDoc} - * Updates the start position of the user. - */ - @Override - public void onMarkerDragEnd(Marker marker) - { - startPosition[0] = (float) marker.getPosition().latitude; - startPosition[1] = (float) marker.getPosition().longitude; - } - - /** - * {@inheritDoc} - */ - @Override - public void onMarkerDrag(Marker marker){} - }); - } - }); - return rootView; - } - - /** - * {@inheritDoc} - * Button onClick listener enabled to detect when to go to next fragment and start PDR recording. - */ - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - // Add button to begin PDR recording and go to recording fragment. - this.button = (Button) getView().findViewById(R.id.startLocationDone); - this.button.setOnClickListener(new View.OnClickListener() { - /** - * {@inheritDoc} - * When button clicked the PDR recording can start and the start position is stored for - * the {@link CorrectionFragment} to display. The {@link RecordingFragment} is loaded. - */ - @Override - public void onClick(View view) { - // Starts recording data from the sensor fusion - sensorFusion.startRecording(); - // Set the start location obtained - sensorFusion.setStartGNSSLatitude(startPosition); - // Navigate to the RecordingFragment - NavDirections action = StartLocationFragmentDirections.actionStartLocationFragmentToRecordingFragment(); - Navigation.findNavController(view).navigate(action); - } - }); - - } - - /** - * Switches the indoor map to the specified floor. - * - * @param floorIndex the index of the floor to switch to - */ - private void switchFloorNU(int floorIndex) { - FloorNK = floorIndex; // Set the current floor index - if (NucleusBuildingManager != null) { - // Call the switchFloor method of the IndoorMapManager to switch to the specified floor - NucleusBuildingManager.getIndoorMapManager().switchFloor(floorIndex); - } - } - -} diff --git a/app/src/main/java/com/openpositioning/PositionMe/fragments/UploadFragment.java b/app/src/main/java/com/openpositioning/PositionMe/fragments/UploadFragment.java index 04cc517d..327c1a10 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/fragments/UploadFragment.java +++ b/app/src/main/java/com/openpositioning/PositionMe/fragments/UploadFragment.java @@ -124,6 +124,9 @@ public void onPositionClicked(int position) { } }); uploadList.setAdapter(listAdapter); + + } } + } \ No newline at end of file diff --git a/app/src/main/res/drawable/background_image_background.xml b/app/src/main/res/drawable/background_image_background.xml new file mode 100644 index 00000000..a8b409b1 --- /dev/null +++ b/app/src/main/res/drawable/background_image_background.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_background.xml b/app/src/main/res/drawable/button_background.xml new file mode 100644 index 00000000..380f51af --- /dev/null +++ b/app/src/main/res/drawable/button_background.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/button_background_light.xml b/app/src/main/res/drawable/button_background_light.xml new file mode 100644 index 00000000..3f24860c --- /dev/null +++ b/app/src/main/res/drawable/button_background_light.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/button_background_light_trans.xml b/app/src/main/res/drawable/button_background_light_trans.xml new file mode 100644 index 00000000..742fca9f --- /dev/null +++ b/app/src/main/res/drawable/button_background_light_trans.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/button_background_replay.xml b/app/src/main/res/drawable/button_background_replay.xml new file mode 100644 index 00000000..af08b64a --- /dev/null +++ b/app/src/main/res/drawable/button_background_replay.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/button_background_trans.xml b/app/src/main/res/drawable/button_background_trans.xml new file mode 100644 index 00000000..6198f5f4 --- /dev/null +++ b/app/src/main/res/drawable/button_background_trans.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/button_background_trans_anti.xml b/app/src/main/res/drawable/button_background_trans_anti.xml new file mode 100644 index 00000000..dac34e2d --- /dev/null +++ b/app/src/main/res/drawable/button_background_trans_anti.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/button_selector.xml b/app/src/main/res/drawable/button_selector.xml new file mode 100644 index 00000000..09af7a9f --- /dev/null +++ b/app/src/main/res/drawable/button_selector.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/drawable/background_image_background.xml b/app/src/main/res/drawable/drawable/background_image_background.xml new file mode 100644 index 00000000..a8b409b1 --- /dev/null +++ b/app/src/main/res/drawable/drawable/background_image_background.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/drawable/baseline_navigation_24.xml b/app/src/main/res/drawable/drawable/baseline_navigation_24.xml new file mode 100644 index 00000000..47d85f4b --- /dev/null +++ b/app/src/main/res/drawable/drawable/baseline_navigation_24.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/drawable/button_background.xml b/app/src/main/res/drawable/drawable/button_background.xml new file mode 100644 index 00000000..380f51af --- /dev/null +++ b/app/src/main/res/drawable/drawable/button_background.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/drawable/button_background_light.xml b/app/src/main/res/drawable/drawable/button_background_light.xml new file mode 100644 index 00000000..3f24860c --- /dev/null +++ b/app/src/main/res/drawable/drawable/button_background_light.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/drawable/button_background_trans.xml b/app/src/main/res/drawable/drawable/button_background_trans.xml new file mode 100644 index 00000000..6198f5f4 --- /dev/null +++ b/app/src/main/res/drawable/drawable/button_background_trans.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/drawable/button_selector.xml b/app/src/main/res/drawable/drawable/button_selector.xml new file mode 100644 index 00000000..09af7a9f --- /dev/null +++ b/app/src/main/res/drawable/drawable/button_selector.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/drawable/eng123.png b/app/src/main/res/drawable/drawable/eng123.png new file mode 100644 index 00000000..c782b2e2 Binary files /dev/null and b/app/src/main/res/drawable/drawable/eng123.png differ diff --git a/app/src/main/res/drawable/drawable/floor_1.png b/app/src/main/res/drawable/drawable/floor_1.png new file mode 100644 index 00000000..e72f6b1e Binary files /dev/null and b/app/src/main/res/drawable/drawable/floor_1.png differ diff --git a/app/src/main/res/drawable/drawable/floor_2.png b/app/src/main/res/drawable/drawable/floor_2.png new file mode 100644 index 00000000..41f19d6b Binary files /dev/null and b/app/src/main/res/drawable/drawable/floor_2.png differ diff --git a/app/src/main/res/drawable/drawable/floor_3.png b/app/src/main/res/drawable/drawable/floor_3.png new file mode 100644 index 00000000..b54d83b4 Binary files /dev/null and b/app/src/main/res/drawable/drawable/floor_3.png differ diff --git a/app/src/main/res/drawable/drawable/floor_lg.png b/app/src/main/res/drawable/drawable/floor_lg.png new file mode 100644 index 00000000..75eb7014 Binary files /dev/null and b/app/src/main/res/drawable/drawable/floor_lg.png differ diff --git a/app/src/main/res/drawable/drawable/floor_ug.png b/app/src/main/res/drawable/drawable/floor_ug.png new file mode 100644 index 00000000..843d8f9a Binary files /dev/null and b/app/src/main/res/drawable/drawable/floor_ug.png differ diff --git a/app/src/main/res/drawable/drawable/ic_baseline_add_location_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_add_location_24.xml new file mode 100644 index 00000000..b3105911 --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_add_location_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_building_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_building_24.xml new file mode 100644 index 00000000..d5b846bc --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_building_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_cancel_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_cancel_24.xml new file mode 100644 index 00000000..1ab7bacb --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_cancel_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_cloud_upload_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_cloud_upload_24.xml new file mode 100644 index 00000000..28965168 --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_cloud_upload_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_data_array_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_data_array_24.xml new file mode 100644 index 00000000..3f9d0b56 --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_data_array_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_directions_walk_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_directions_walk_24.xml new file mode 100644 index 00000000..b726e4d5 --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_directions_walk_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_done_all_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_done_all_24.xml new file mode 100644 index 00000000..648f00d7 --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_done_all_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_download_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_download_24.xml new file mode 100644 index 00000000..1f61509e --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_download_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_elevator_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_elevator_24.xml new file mode 100644 index 00000000..ad21aeb7 --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_elevator_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_fast_forward_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_fast_forward_24.xml new file mode 100644 index 00000000..48197896 --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_fast_forward_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_fast_rewind_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_fast_rewind_24.xml new file mode 100644 index 00000000..04538918 --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_fast_rewind_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_folder_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_folder_24.xml new file mode 100644 index 00000000..dc6b0802 --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_folder_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_info_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_info_24.xml new file mode 100644 index 00000000..9ad76930 --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_info_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_location_searching_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_location_searching_24.xml new file mode 100644 index 00000000..be684270 --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_location_searching_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_navigation_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_navigation_24.xml new file mode 100644 index 00000000..409078ae --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_navigation_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_pause_circle_filled_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_pause_circle_filled_24.xml new file mode 100644 index 00000000..abeb2212 --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_pause_circle_filled_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_play_arrow_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_play_arrow_24.xml new file mode 100644 index 00000000..2451d012 --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_play_arrow_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_play_circle_filled_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_play_circle_filled_24.xml new file mode 100644 index 00000000..24817a67 --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_play_circle_filled_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_play_circle_filled_24_b.xml b/app/src/main/res/drawable/drawable/ic_baseline_play_circle_filled_24_b.xml new file mode 100644 index 00000000..46b62729 --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_play_circle_filled_24_b.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_red_dot_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_red_dot_24.xml new file mode 100644 index 00000000..b5b71d7d --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_red_dot_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_settings_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_settings_24.xml new file mode 100644 index 00000000..41a82ede --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_settings_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_settings_24_blue.xml b/app/src/main/res/drawable/drawable/ic_baseline_settings_24_blue.xml new file mode 100644 index 00000000..7a5be341 --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_settings_24_blue.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_stop_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_stop_24.xml new file mode 100644 index 00000000..d099f1aa --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_stop_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_upload_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_upload_24.xml new file mode 100644 index 00000000..c1e163b7 --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_upload_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/drawable/ic_baseline_wifi_24.xml b/app/src/main/res/drawable/drawable/ic_baseline_wifi_24.xml new file mode 100644 index 00000000..884c152c --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_baseline_wifi_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/drawable/ic_launcher_icon.xml b/app/src/main/res/drawable/drawable/ic_launcher_icon.xml new file mode 100644 index 00000000..15dcfce9 --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_launcher_icon.xml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/drawable/ic_launcher_icon_map_foreground.xml b/app/src/main/res/drawable/drawable/ic_launcher_icon_map_foreground.xml new file mode 100644 index 00000000..15dcfce9 --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_launcher_icon_map_foreground.xml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/drawable/ic_launcher_simple_foreground.xml b/app/src/main/res/drawable/drawable/ic_launcher_simple_foreground.xml new file mode 100644 index 00000000..cca68804 --- /dev/null +++ b/app/src/main/res/drawable/drawable/ic_launcher_simple_foreground.xml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/drawable/library1.png b/app/src/main/res/drawable/drawable/library1.png new file mode 100644 index 00000000..d55850b8 Binary files /dev/null and b/app/src/main/res/drawable/drawable/library1.png differ diff --git a/app/src/main/res/drawable/drawable/library2.png b/app/src/main/res/drawable/drawable/library2.png new file mode 100644 index 00000000..27fa2b21 Binary files /dev/null and b/app/src/main/res/drawable/drawable/library2.png differ diff --git a/app/src/main/res/drawable/drawable/library3.png b/app/src/main/res/drawable/drawable/library3.png new file mode 100644 index 00000000..a7eff2bc Binary files /dev/null and b/app/src/main/res/drawable/drawable/library3.png differ diff --git a/app/src/main/res/drawable/drawable/libraryg.png b/app/src/main/res/drawable/drawable/libraryg.png new file mode 100644 index 00000000..e7f185df Binary files /dev/null and b/app/src/main/res/drawable/drawable/libraryg.png differ diff --git a/app/src/main/res/drawable/drawable/nucleus1.png b/app/src/main/res/drawable/drawable/nucleus1.png new file mode 100644 index 00000000..0c63ad5e Binary files /dev/null and b/app/src/main/res/drawable/drawable/nucleus1.png differ diff --git a/app/src/main/res/drawable/drawable/nucleus2.png b/app/src/main/res/drawable/drawable/nucleus2.png new file mode 100644 index 00000000..5c9584eb Binary files /dev/null and b/app/src/main/res/drawable/drawable/nucleus2.png differ diff --git a/app/src/main/res/drawable/drawable/nucleus3.png b/app/src/main/res/drawable/drawable/nucleus3.png new file mode 100644 index 00000000..4df9e09b Binary files /dev/null and b/app/src/main/res/drawable/drawable/nucleus3.png differ diff --git a/app/src/main/res/drawable/drawable/nucleusg.png b/app/src/main/res/drawable/drawable/nucleusg.png new file mode 100644 index 00000000..6454a1f7 Binary files /dev/null and b/app/src/main/res/drawable/drawable/nucleusg.png differ diff --git a/app/src/main/res/drawable/drawable/nucleusground.png b/app/src/main/res/drawable/drawable/nucleusground.png new file mode 100644 index 00000000..11502ce1 Binary files /dev/null and b/app/src/main/res/drawable/drawable/nucleusground.png differ diff --git a/app/src/main/res/drawable/drawable/nucleuslg.png b/app/src/main/res/drawable/drawable/nucleuslg.png new file mode 100644 index 00000000..872c1dbc Binary files /dev/null and b/app/src/main/res/drawable/drawable/nucleuslg.png differ diff --git a/app/src/main/res/drawable/drawable/round__lighblue_button.xml b/app/src/main/res/drawable/drawable/round__lighblue_button.xml new file mode 100644 index 00000000..de20f4af --- /dev/null +++ b/app/src/main/res/drawable/drawable/round__lighblue_button.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/drawable/round_button.xml b/app/src/main/res/drawable/drawable/round_button.xml new file mode 100644 index 00000000..884e4e24 --- /dev/null +++ b/app/src/main/res/drawable/drawable/round_button.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/drawable/rounded_button.xml b/app/src/main/res/drawable/drawable/rounded_button.xml new file mode 100644 index 00000000..8ee78595 --- /dev/null +++ b/app/src/main/res/drawable/drawable/rounded_button.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/drawable/rounded_button_black.xml b/app/src/main/res/drawable/drawable/rounded_button_black.xml new file mode 100644 index 00000000..4243754a --- /dev/null +++ b/app/src/main/res/drawable/drawable/rounded_button_black.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/drawable/rounded_corner.xml b/app/src/main/res/drawable/drawable/rounded_corner.xml new file mode 100644 index 00000000..1ddecded --- /dev/null +++ b/app/src/main/res/drawable/drawable/rounded_corner.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/drawable/rounded_corner_lightblue.xml b/app/src/main/res/drawable/drawable/rounded_corner_lightblue.xml new file mode 100644 index 00000000..a9bda597 --- /dev/null +++ b/app/src/main/res/drawable/drawable/rounded_corner_lightblue.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/drawable/rounded_corner_pastel_blue.xml b/app/src/main/res/drawable/drawable/rounded_corner_pastel_blue.xml new file mode 100644 index 00000000..f179da0a --- /dev/null +++ b/app/src/main/res/drawable/drawable/rounded_corner_pastel_blue.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/eng123.png b/app/src/main/res/drawable/eng123.png new file mode 100644 index 00000000..c782b2e2 Binary files /dev/null and b/app/src/main/res/drawable/eng123.png differ diff --git a/app/src/main/res/drawable/f0.png b/app/src/main/res/drawable/f0.png new file mode 100644 index 00000000..f0382e9a Binary files /dev/null and b/app/src/main/res/drawable/f0.png differ diff --git a/app/src/main/res/drawable/f0g.png b/app/src/main/res/drawable/f0g.png new file mode 100644 index 00000000..5240a215 Binary files /dev/null and b/app/src/main/res/drawable/f0g.png differ diff --git a/app/src/main/res/drawable/f1.png b/app/src/main/res/drawable/f1.png new file mode 100644 index 00000000..5240a215 Binary files /dev/null and b/app/src/main/res/drawable/f1.png differ diff --git a/app/src/main/res/drawable/f2.png b/app/src/main/res/drawable/f2.png new file mode 100644 index 00000000..5240a215 Binary files /dev/null and b/app/src/main/res/drawable/f2.png differ diff --git a/app/src/main/res/drawable/f3.png b/app/src/main/res/drawable/f3.png new file mode 100644 index 00000000..450ed627 Binary files /dev/null and b/app/src/main/res/drawable/f3.png differ diff --git a/app/src/main/res/drawable/h0g.png b/app/src/main/res/drawable/h0g.png new file mode 100644 index 00000000..e5dbb746 Binary files /dev/null and b/app/src/main/res/drawable/h0g.png differ diff --git a/app/src/main/res/drawable/h1.png b/app/src/main/res/drawable/h1.png new file mode 100644 index 00000000..e5dbb746 Binary files /dev/null and b/app/src/main/res/drawable/h1.png differ diff --git a/app/src/main/res/drawable/h2.png b/app/src/main/res/drawable/h2.png new file mode 100644 index 00000000..964a6049 Binary files /dev/null and b/app/src/main/res/drawable/h2.png differ diff --git a/app/src/main/res/drawable/ic_baseline_fast_forward_24.xml b/app/src/main/res/drawable/ic_baseline_fast_forward_24.xml new file mode 100644 index 00000000..b658ead5 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_fast_forward_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_baseline_fast_rewind_24.xml b/app/src/main/res/drawable/ic_baseline_fast_rewind_24.xml new file mode 100644 index 00000000..b8fdf434 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_fast_rewind_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_baseline_pause_circle_filled_24.xml b/app/src/main/res/drawable/ic_baseline_pause_circle_filled_24.xml new file mode 100644 index 00000000..9a7f0e2e --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_pause_circle_filled_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_baseline_play_circle_filled_24_b.xml b/app/src/main/res/drawable/ic_baseline_play_circle_filled_24_b.xml new file mode 100644 index 00000000..5e01a0e1 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_play_circle_filled_24_b.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/round_button.xml b/app/src/main/res/drawable/round_button.xml index 33dc32c8..884e4e24 100644 --- a/app/src/main/res/drawable/round_button.xml +++ b/app/src/main/res/drawable/round_button.xml @@ -2,12 +2,12 @@ - + - + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 8e35b136..e0e67e03 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -6,26 +6,31 @@ android:layout_height="match_parent" tools:context=".MainActivity"> + + - - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_correction.xml b/app/src/main/res/layout/fragment_correction.xml index 1537a6dd..8973bc23 100644 --- a/app/src/main/res/layout/fragment_correction.xml +++ b/app/src/main/res/layout/fragment_correction.xml @@ -113,7 +113,9 @@ android:layout_height="70dp" android:layout_marginBottom="24dp" android:text="@string/done" + android:textColor="@color/cardview_light_background" android:textSize="24sp" + android:background="@drawable/button_background" app:icon="@drawable/ic_baseline_done_all_24" app:iconGravity="start" app:iconSize="30dp" @@ -126,8 +128,10 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:contentDescription="Path drawing" - tools:layout_editor_absoluteX="86dp" - tools:layout_editor_absoluteY="-128dp" /> + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/layout/fragment_files.xml b/app/src/main/res/layout/fragment_files.xml index 75ea4aac..fad27a62 100644 --- a/app/src/main/res/layout/fragment_files.xml +++ b/app/src/main/res/layout/fragment_files.xml @@ -30,6 +30,20 @@ +