() {{
@@ -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