diff --git a/app/src/main/java/com/openpositioning/PositionMe/data/local/GnssSectionParser.java b/app/src/main/java/com/openpositioning/PositionMe/data/local/GnssSectionParser.java new file mode 100644 index 00000000..2117dc41 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/data/local/GnssSectionParser.java @@ -0,0 +1,22 @@ +package com.openpositioning.PositionMe.data.local; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; + +import java.util.ArrayList; +import java.util.List; + +/** Parses GNSS records from a JSON array. */ +class GnssSectionParser { + List parse(JsonArray gnssArray) { + List gnssList = new ArrayList<>(); + if (gnssArray == null) return gnssList; + Gson gson = new Gson(); + for (int i = 0; i < gnssArray.size(); i++) { + JsonTrajectoryParser.GnssRecord record = + gson.fromJson(gnssArray.get(i), JsonTrajectoryParser.GnssRecord.class); + gnssList.add(record); + } + return gnssList; + } +} diff --git a/app/src/main/java/com/openpositioning/PositionMe/data/local/ImuSectionParser.java b/app/src/main/java/com/openpositioning/PositionMe/data/local/ImuSectionParser.java new file mode 100644 index 00000000..14201202 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/data/local/ImuSectionParser.java @@ -0,0 +1,22 @@ +package com.openpositioning.PositionMe.data.local; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; + +import java.util.ArrayList; +import java.util.List; + +/** Parses IMU records from a JSON array. */ +class ImuSectionParser { + List parse(JsonArray imuArray) { + List imuList = new ArrayList<>(); + if (imuArray == null) return imuList; + Gson gson = new Gson(); + for (int i = 0; i < imuArray.size(); i++) { + JsonTrajectoryParser.ImuRecord record = + gson.fromJson(imuArray.get(i), JsonTrajectoryParser.ImuRecord.class); + imuList.add(record); + } + return imuList; + } +} diff --git a/app/src/main/java/com/openpositioning/PositionMe/data/local/JsonTrajectoryParser.java b/app/src/main/java/com/openpositioning/PositionMe/data/local/JsonTrajectoryParser.java new file mode 100644 index 00000000..6db4942f --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/data/local/JsonTrajectoryParser.java @@ -0,0 +1,135 @@ +package com.openpositioning.PositionMe.data.local; + +import android.content.Context; +import android.hardware.SensorManager; +import android.util.Log; + +import com.google.android.gms.maps.model.LatLng; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.BufferedReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Parses trajectory data stored in JSON files. + */ +public class JsonTrajectoryParser implements TrajectoryParser { + + private static final String TAG = "JsonTrajectoryParser"; + + static class ImuRecord { + long relativeTimestamp; + float accX, accY, accZ; + float gyrX, gyrY, gyrZ; + float rotationVectorX, rotationVectorY, rotationVectorZ, rotationVectorW; + } + + static class PdrRecord { + long relativeTimestamp; + float x, y; + } + + static class GnssRecord { + long relativeTimestamp; + double latitude, longitude; + } + + private final ImuSectionParser imuParser = new ImuSectionParser(); + private final PdrSectionParser pdrParser = new PdrSectionParser(); + private final GnssSectionParser gnssParser = new GnssSectionParser(); + + @Override + public List parse(Path file, Context context, + double originLat, double originLng) { + List result = new ArrayList<>(); + try { + if (!Files.exists(file) || !Files.isReadable(file)) { + Log.e(TAG, "File does NOT exist or is not readable: " + file); + return result; + } + + BufferedReader br = Files.newBufferedReader(file); + JsonObject root = new JsonParser().parse(br).getAsJsonObject(); + br.close(); + + long startTimestamp = root.has("startTimestamp") + ? root.get("startTimestamp").getAsLong() : 0; + + List imuList = imuParser.parse(root.getAsJsonArray("imuData")); + List pdrList = pdrParser.parse(root.getAsJsonArray("pdrData")); + List gnssList = gnssParser.parse(root.getAsJsonArray("gnssData")); + + for (int i = 0; i < pdrList.size(); i++) { + PdrRecord pdr = pdrList.get(i); + + ImuRecord closestImu = findClosestImuRecord(imuList, pdr.relativeTimestamp); + float orientationDeg = closestImu != null ? computeOrientationFromRotationVector( + closestImu.rotationVectorX, + closestImu.rotationVectorY, + closestImu.rotationVectorZ, + closestImu.rotationVectorW, + context + ) : 0f; + + float speed = 0f; + if (i > 0) { + PdrRecord prev = pdrList.get(i - 1); + double dt = (pdr.relativeTimestamp - prev.relativeTimestamp) / 1000.0; + double dx = pdr.x - prev.x; + double dy = pdr.y - prev.y; + double distance = Math.sqrt(dx * dx + dy * dy); + if (dt > 0) speed = (float) (distance / dt); + } + + double lat = originLat + pdr.y * 1E-5; + double lng = originLng + pdr.x * 1E-5; + LatLng pdrLocation = new LatLng(lat, lng); + + GnssRecord closestGnss = findClosestGnssRecord(gnssList, pdr.relativeTimestamp); + LatLng gnssLocation = closestGnss != null ? + new LatLng(closestGnss.latitude, closestGnss.longitude) : null; + + result.add(new ReplayPoint(pdrLocation, gnssLocation, orientationDeg, + speed, pdr.relativeTimestamp)); + } + + Collections.sort(result, Comparator.comparingLong(rp -> rp.timestamp)); + } catch (Exception e) { + Log.e(TAG, "Error parsing trajectory file!", e); + } + + return result; + } + + private ImuRecord findClosestImuRecord(List imuList, long targetTimestamp) { + return imuList.stream().min( + Comparator.comparingLong(imu -> Math.abs(imu.relativeTimestamp - targetTimestamp))) + .orElse(null); + } + + private GnssRecord findClosestGnssRecord(List gnssList, long targetTimestamp) { + return gnssList.stream().min( + Comparator.comparingLong(gnss -> Math.abs(gnss.relativeTimestamp - targetTimestamp))) + .orElse(null); + } + + private float computeOrientationFromRotationVector(float rx, float ry, float rz, float rw, + Context context) { + float[] rotationVector = new float[]{rx, ry, rz, rw}; + float[] rotationMatrix = new float[9]; + float[] orientationAngles = new float[3]; + + SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVector); + SensorManager.getOrientation(rotationMatrix, orientationAngles); + + float azimuthDeg = (float) Math.toDegrees(orientationAngles[0]); + return azimuthDeg < 0 ? azimuthDeg + 360.0f : azimuthDeg; + } +} diff --git a/app/src/main/java/com/openpositioning/PositionMe/data/local/PdrSectionParser.java b/app/src/main/java/com/openpositioning/PositionMe/data/local/PdrSectionParser.java new file mode 100644 index 00000000..bc36ac97 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/data/local/PdrSectionParser.java @@ -0,0 +1,22 @@ +package com.openpositioning.PositionMe.data.local; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; + +import java.util.ArrayList; +import java.util.List; + +/** Parses PDR records from a JSON array. */ +class PdrSectionParser { + List parse(JsonArray pdrArray) { + List pdrList = new ArrayList<>(); + if (pdrArray == null) return pdrList; + Gson gson = new Gson(); + for (int i = 0; i < pdrArray.size(); i++) { + JsonTrajectoryParser.PdrRecord record = + gson.fromJson(pdrArray.get(i), JsonTrajectoryParser.PdrRecord.class); + pdrList.add(record); + } + return pdrList; + } +} diff --git a/app/src/main/java/com/openpositioning/PositionMe/data/local/TrajParser.java b/app/src/main/java/com/openpositioning/PositionMe/data/local/TrajParser.java deleted file mode 100644 index cd45764b..00000000 --- a/app/src/main/java/com/openpositioning/PositionMe/data/local/TrajParser.java +++ /dev/null @@ -1,256 +0,0 @@ -package com.openpositioning.PositionMe.data.local; - -import android.content.Context; -import android.hardware.SensorManager; -import android.util.Log; - -import com.google.android.gms.maps.model.LatLng; -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.openpositioning.PositionMe.presentation.fragment.ReplayFragment; -import com.openpositioning.PositionMe.sensors.SensorFusion; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -/** - * Handles parsing of trajectory data stored in JSON files, combining IMU, PDR, and GNSS data - * to reconstruct motion paths. - * - *

- * The **TrajParser** is primarily responsible for processing recorded trajectory data and - * reconstructing motion information, including estimated positions, GNSS coordinates, speed, and orientation. - * It does this by reading a JSON file containing: - *

- *
    - *
  • IMU (Inertial Measurement Unit) data
  • - *
  • PDR (Pedestrian Dead Reckoning) position data
  • - *
  • GNSS (Global Navigation Satellite System) location data
  • - *
- * - *

- * **Usage in Module 'PositionMe.app.main':** - *

- *
    - *
  • **ReplayFragment** - Calls `parseTrajectoryData()` to read recorded trajectory files and process movement.
  • - *
  • Stores parsed trajectory data as `ReplayPoint` objects.
  • - *
  • Provides data for updating map visualizations in `ReplayFragment`.
  • - *
- * - * @see ReplayFragment which uses parsed trajectory data for visualization. - * @see SensorFusion for motion processing and sensor integration. - * @see com.openpositioning.PositionMe.presentation.fragment.ReplayFragment for implementation details. - * - * @author Shu Gu - * @author Lin Cheng - */ -public class TrajParser { - - private static final String TAG = "TrajParser"; - - /** - * Represents a single replay point containing estimated PDR position, GNSS location, - * orientation, speed, and timestamp. - */ - public static class ReplayPoint { - public LatLng pdrLocation; // PDR-derived location estimate - public LatLng gnssLocation; // GNSS location (may be null if unavailable) - public float orientation; // Orientation in degrees - public float speed; // Speed in meters per second - public long timestamp; // Relative timestamp - - /** - * Constructs a ReplayPoint. - * - * @param pdrLocation The pedestrian dead reckoning (PDR) location. - * @param gnssLocation The GNSS location, or null if unavailable. - * @param orientation The orientation angle in degrees. - * @param speed The speed in meters per second. - * @param timestamp The timestamp associated with this point. - */ - public ReplayPoint(LatLng pdrLocation, LatLng gnssLocation, float orientation, float speed, long timestamp) { - this.pdrLocation = pdrLocation; - this.gnssLocation = gnssLocation; - this.orientation = orientation; - this.speed = speed; - this.timestamp = timestamp; - } - } - - /** Represents an IMU (Inertial Measurement Unit) data record used for orientation calculations. */ - public static class ImuRecord { - public long relativeTimestamp; - public float accX, accY, accZ; // Accelerometer values - public float gyrX, gyrY, gyrZ; // Gyroscope values - public float rotationVectorX, rotationVectorY, rotationVectorZ, rotationVectorW; // Rotation quaternion - } - - /** Represents a Pedestrian Dead Reckoning (PDR) data record storing position shifts over time. */ - public static class PdrRecord { - public long relativeTimestamp; - public float x, y; // Position relative to the starting point - } - - /** Represents a GNSS (Global Navigation Satellite System) data record with latitude/longitude. */ - public static class GnssRecord { - public long relativeTimestamp; - public double latitude, longitude; // GNSS coordinates - } - - /** - * Parses trajectory data from a JSON file and reconstructs a list of replay points. - * - *

- * This method processes a trajectory log file, extracting IMU, PDR, and GNSS records, - * and uses them to generate **ReplayPoint** objects. Each point contains: - *

- *
    - *
  • Estimated PDR-based position.
  • - *
  • GNSS location (if available).
  • - *
  • Computed orientation using rotation vectors.
  • - *
  • Speed estimation based on movement data.
  • - *
- * - * @param filePath Path to the JSON file containing trajectory data. - * @param context Android application context (used for sensor processing). - * @param originLat Latitude of the reference origin. - * @param originLng Longitude of the reference origin. - * @return A list of parsed {@link ReplayPoint} objects. - */ - public static List parseTrajectoryData(String filePath, Context context, - double originLat, double originLng) { - List result = new ArrayList<>(); - - try { - File file = new File(filePath); - if (!file.exists()) { - Log.e(TAG, "File does NOT exist: " + filePath); - return result; - } - if (!file.canRead()) { - Log.e(TAG, "File is NOT readable: " + filePath); - return result; - } - - BufferedReader br = new BufferedReader(new FileReader(file)); - JsonObject root = new JsonParser().parse(br).getAsJsonObject(); - br.close(); - - Log.i(TAG, "Successfully read trajectory file: " + filePath); - - long startTimestamp = root.has("startTimestamp") ? root.get("startTimestamp").getAsLong() : 0; - - List imuList = parseImuData(root.getAsJsonArray("imuData")); - List pdrList = parsePdrData(root.getAsJsonArray("pdrData")); - List gnssList = parseGnssData(root.getAsJsonArray("gnssData")); - - Log.i(TAG, "Parsed data - IMU: " + imuList.size() + " records, PDR: " - + pdrList.size() + " records, GNSS: " + gnssList.size() + " records"); - - for (int i = 0; i < pdrList.size(); i++) { - PdrRecord pdr = pdrList.get(i); - - ImuRecord closestImu = findClosestImuRecord(imuList, pdr.relativeTimestamp); - float orientationDeg = closestImu != null ? computeOrientationFromRotationVector( - closestImu.rotationVectorX, - closestImu.rotationVectorY, - closestImu.rotationVectorZ, - closestImu.rotationVectorW, - context - ) : 0f; - - float speed = 0f; - if (i > 0) { - PdrRecord prev = pdrList.get(i - 1); - double dt = (pdr.relativeTimestamp - prev.relativeTimestamp) / 1000.0; - double dx = pdr.x - prev.x; - double dy = pdr.y - prev.y; - double distance = Math.sqrt(dx * dx + dy * dy); - if (dt > 0) speed = (float) (distance / dt); - } - - - double lat = originLat + pdr.y * 1E-5; - double lng = originLng + pdr.x * 1E-5; - LatLng pdrLocation = new LatLng(lat, lng); - - GnssRecord closestGnss = findClosestGnssRecord(gnssList, pdr.relativeTimestamp); - LatLng gnssLocation = closestGnss != null ? - new LatLng(closestGnss.latitude, closestGnss.longitude) : null; - - result.add(new ReplayPoint(pdrLocation, gnssLocation, orientationDeg, - 0f, pdr.relativeTimestamp)); - } - - Collections.sort(result, Comparator.comparingLong(rp -> rp.timestamp)); - - Log.i(TAG, "Final ReplayPoints count: " + result.size()); - - } catch (Exception e) { - Log.e(TAG, "Error parsing trajectory file!", e); - } - - return result; - } -/** Parses IMU data from JSON. */ -private static List parseImuData(JsonArray imuArray) { - List imuList = new ArrayList<>(); - if (imuArray == null) return imuList; - Gson gson = new Gson(); - for (int i = 0; i < imuArray.size(); i++) { - ImuRecord record = gson.fromJson(imuArray.get(i), ImuRecord.class); - imuList.add(record); - } - return imuList; -}/** Parses PDR data from JSON. */ -private static List parsePdrData(JsonArray pdrArray) { - List pdrList = new ArrayList<>(); - if (pdrArray == null) return pdrList; - Gson gson = new Gson(); - for (int i = 0; i < pdrArray.size(); i++) { - PdrRecord record = gson.fromJson(pdrArray.get(i), PdrRecord.class); - pdrList.add(record); - } - return pdrList; -}/** Parses GNSS data from JSON. */ -private static List parseGnssData(JsonArray gnssArray) { - List gnssList = new ArrayList<>(); - if (gnssArray == null) return gnssList; - Gson gson = new Gson(); - for (int i = 0; i < gnssArray.size(); i++) { - GnssRecord record = gson.fromJson(gnssArray.get(i), GnssRecord.class); - gnssList.add(record); - } - return gnssList; -}/** Finds the closest IMU record to the given timestamp. */ -private static ImuRecord findClosestImuRecord(List imuList, long targetTimestamp) { - return imuList.stream().min(Comparator.comparingLong(imu -> Math.abs(imu.relativeTimestamp - targetTimestamp))) - .orElse(null); - -}/** Finds the closest GNSS record to the given timestamp. */ -private static GnssRecord findClosestGnssRecord(List gnssList, long targetTimestamp) { - return gnssList.stream().min(Comparator.comparingLong(gnss -> Math.abs(gnss.relativeTimestamp - targetTimestamp))) - .orElse(null); - -}/** Computes the orientation from a rotation vector. */ -private static float computeOrientationFromRotationVector(float rx, float ry, float rz, float rw, Context context) { - float[] rotationVector = new float[]{rx, ry, rz, rw}; - float[] rotationMatrix = new float[9]; - float[] orientationAngles = new float[3]; - - SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); - SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVector); - SensorManager.getOrientation(rotationMatrix, orientationAngles); - - float azimuthDeg = (float) Math.toDegrees(orientationAngles[0]); - return azimuthDeg < 0 ? azimuthDeg + 360.0f : azimuthDeg; -} - -} \ No newline at end of file diff --git a/app/src/main/java/com/openpositioning/PositionMe/data/local/TrajectoryParser.java b/app/src/main/java/com/openpositioning/PositionMe/data/local/TrajectoryParser.java new file mode 100644 index 00000000..3513277a --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/data/local/TrajectoryParser.java @@ -0,0 +1,46 @@ +package com.openpositioning.PositionMe.data.local; + +import android.content.Context; + +import com.google.android.gms.maps.model.LatLng; + +import java.nio.file.Path; +import java.util.List; + +/** + * Interface for parsing trajectory files into replay points. + */ +public interface TrajectoryParser { + + /** + * Represents a single replay point containing estimated PDR position, + * optional GNSS location, orientation, speed and timestamp. + */ + class ReplayPoint { + public LatLng pdrLocation; + public LatLng gnssLocation; + public float orientation; + public float speed; + public long timestamp; + + public ReplayPoint(LatLng pdrLocation, LatLng gnssLocation, + float orientation, float speed, long timestamp) { + this.pdrLocation = pdrLocation; + this.gnssLocation = gnssLocation; + this.orientation = orientation; + this.speed = speed; + this.timestamp = timestamp; + } + } + + /** + * Parse a trajectory file and return a list of replay points. + * + * @param file path to the trajectory file + * @param ctx Android context for sensor calculations + * @param originLat reference latitude + * @param originLng reference longitude + * @return list of parsed replay points + */ + List parse(Path file, Context ctx, double originLat, double originLng); +} diff --git a/app/src/main/java/com/openpositioning/PositionMe/data/local/TrajectoryParserFactory.java b/app/src/main/java/com/openpositioning/PositionMe/data/local/TrajectoryParserFactory.java new file mode 100644 index 00000000..b8ae9de7 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/data/local/TrajectoryParserFactory.java @@ -0,0 +1,10 @@ +package com.openpositioning.PositionMe.data.local; + +/** Factory for creating trajectory parsers. */ +public final class TrajectoryParserFactory { + private TrajectoryParserFactory() {} + + public static TrajectoryParser createJsonParser() { + return new JsonTrajectoryParser(); + } +} diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/ReplayFragment.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/ReplayFragment.java index 6792566a..746c918a 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/ReplayFragment.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/ReplayFragment.java @@ -17,7 +17,8 @@ import com.google.android.gms.maps.model.LatLng; import com.openpositioning.PositionMe.R; import com.openpositioning.PositionMe.presentation.activity.ReplayActivity; -import com.openpositioning.PositionMe.data.local.TrajParser; +import com.openpositioning.PositionMe.data.local.TrajectoryParser; +import com.openpositioning.PositionMe.data.local.TrajectoryParserFactory; import java.io.File; import java.util.ArrayList; @@ -39,7 +40,7 @@ * * @see TrajectoryMapFragment The map fragment displaying the trajectory. * @see ReplayActivity The activity managing the replay workflow. - * @see TrajParser Utility class for parsing trajectory data. + * @see TrajectoryParser Parser interface for trajectory data. * * @author Shu Gu */ @@ -61,7 +62,7 @@ public class ReplayFragment extends Fragment { // Playback-related private final Handler playbackHandler = new Handler(); private final long PLAYBACK_INTERVAL_MS = 500; // milliseconds - private List replayData = new ArrayList<>(); + private final List replayData = new ArrayList<>(); private int currentIndex = 0; private boolean isPlaying = false; @@ -95,8 +96,10 @@ public void onCreate(@Nullable Bundle savedInstanceState) { Log.i(TAG, "Trajectory file confirmed to exist and is readable."); - // Parse the JSON file and prepare replayData using TrajParser - replayData = TrajParser.parseTrajectoryData(filePath, requireContext(), initialLat, initialLon); + // Parse the JSON file and prepare replayData using TrajectoryParser + TrajectoryParser parser = TrajectoryParserFactory.createJsonParser(); + replayData.clear(); + replayData.addAll(parser.parse(java.nio.file.Paths.get(filePath), requireContext(), initialLat, initialLon)); // Log the number of parsed points if (replayData != null && !replayData.isEmpty()) { @@ -234,8 +237,8 @@ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { /** * Checks if any ReplayPoint contains a non-null gnssLocation. */ - private boolean hasAnyGnssData(List data) { - for (TrajParser.ReplayPoint point : data) { + private boolean hasAnyGnssData(List data) { + for (TrajectoryParser.ReplayPoint point : data) { if (point.gnssLocation != null) { return true; } @@ -280,8 +283,8 @@ private void setupInitialMapPosition(float latitude, float longitude) { /** * Retrieve the first available GNSS location from the replay data. */ - private LatLng getFirstGnssLocation(List data) { - for (TrajParser.ReplayPoint point : data) { + private LatLng getFirstGnssLocation(List data) { + for (TrajectoryParser.ReplayPoint point : data) { if (point.gnssLocation != null) { return new LatLng(replayData.get(0).gnssLocation.latitude, replayData.get(0).gnssLocation.longitude); } @@ -332,7 +335,7 @@ private void updateMapForIndex(int newIndex) { // Clear everything and redraw up to newIndex trajectoryMapFragment.clearMapAndReset(); for (int i = 0; i <= newIndex; i++) { - TrajParser.ReplayPoint p = replayData.get(i); + TrajectoryParser.ReplayPoint p = replayData.get(i); trajectoryMapFragment.updateUserLocation(p.pdrLocation, p.orientation); if (p.gnssLocation != null) { trajectoryMapFragment.updateGNSS(p.gnssLocation); @@ -340,7 +343,7 @@ private void updateMapForIndex(int newIndex) { } } else { // Normal sequential forward step: add just the new point - TrajParser.ReplayPoint p = replayData.get(newIndex); + TrajectoryParser.ReplayPoint p = replayData.get(newIndex); trajectoryMapFragment.updateUserLocation(p.pdrLocation, p.orientation); if (p.gnssLocation != null) { trajectoryMapFragment.updateGNSS(p.gnssLocation);