diff --git a/.gitignore b/.gitignore index d4c3a57e..55e62e88 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ .cxx local.properties /.idea/ +secrets.properties diff --git a/app/build.gradle b/app/build.gradle index 3e29b13f..69f12a38 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,9 +3,9 @@ plugins { id 'com.google.gms.google-services' id 'androidx.navigation.safeargs' id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' + id 'com.google.protobuf' version '0.9.4' } -// (Optional) load local secrets file: def localProperties = new Properties() def localPropertiesFile = rootProject.file('secrets.properties') if (localPropertiesFile.exists()) { @@ -14,18 +14,19 @@ if (localPropertiesFile.exists()) { android { namespace "com.openpositioning.PositionMe" - compileSdk 34 + compileSdk 35 defaultConfig { applicationId "com.openpositioning.PositionMe" minSdk 28 - targetSdk 34 + targetSdk 35 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - // Example of referencing secrets (if you use secrets.properties): + manifestPlaceholders = [MAPS_API_KEY: localProperties['MAPS_API_KEY'] ?: ""] + buildConfigField "String", "MAPS_API_KEY", "\"${localProperties['MAPS_API_KEY'] ?: ''}\"" buildConfigField "String", "OPENPOSITIONING_API_KEY", @@ -34,12 +35,6 @@ android { "\"${localProperties['OPENPOSITIONING_MASTER_KEY'] ?: ''}\"" } - buildFeatures { - // For example: - // compose true // if you want Jetpack Compose - // viewBinding true - } - buildFeatures { buildConfig true } @@ -57,9 +52,22 @@ android { } } +protobuf { + protoc { + artifact = 'com.google.protobuf:protoc:3.25.3' + } + generateProtoTasks { + all().each { task -> + task.builtins { + java { + } + } + } + } +} + dependencies { - // Core AndroidX - implementation 'androidx.appcompat:appcompat:1.7.0-alpha03' // or stable: 1.6.1 + implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.constraintlayout:constraintlayout:2.2.0' implementation 'androidx.preference:preference:1.2.1' @@ -67,29 +75,22 @@ dependencies { implementation 'com.android.volley:volley:1.2.1' implementation 'androidx.gridlayout:gridlayout:1.0.0' - // Material Components (Material 3 support is in 1.12.0+) - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.2.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' implementation 'com.google.android.material:material:1.12.0' - implementation 'com.google.protobuf:protobuf-java:3.0.0' + implementation 'com.google.protobuf:protobuf-java:3.25.3' + implementation "com.google.protobuf:protobuf-java-util:3.25.3" + implementation 'com.squareup.okhttp3:okhttp:4.10.0' - implementation "com.google.protobuf:protobuf-java-util:3.0.0" implementation "com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava" implementation 'com.google.android.gms:play-services-maps:19.0.0' + implementation 'com.google.android.gms:play-services-location:21.0.1' + implementation 'androidx.biometric:biometric:1.2.0-alpha04' - // Navigation components def nav_version = "2.8.6" implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" - // Optional: Jetpack Compose (Material 3) - // implementation "androidx.compose.material3:material3:1.3.1" - // implementation "androidx.activity:activity-compose:1.7.2" - - // Testing testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.2.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' -} + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/openpositioning/PositionMe/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/openpositioning/PositionMe/ExampleInstrumentedTest.java index ace7cabb..73d50b9f 100644 --- a/app/src/androidTest/java/com/openpositioning/PositionMe/ExampleInstrumentedTest.java +++ b/app/src/androidTest/java/com/openpositioning/PositionMe/ExampleInstrumentedTest.java @@ -10,11 +10,8 @@ import static org.junit.Assert.*; -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ +// Instrumented test, which will execute on an Android device. +// @see Testing documentation @RunWith(AndroidJUnit4.class) public class ExampleInstrumentedTest { @Test @@ -23,4 +20,5 @@ public void useAppContext() { Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); assertEquals("com.openpositioning.PositionMe", appContext.getPackageName()); } -} \ No newline at end of file +} + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 678711fd..0ef91dcc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,68 +1,42 @@ + xmlns:tools="http://schemas.android.com/tools" + > - + + - - - - - + + - - - - - - - - - + - - + + - + + - - - - - - - - - - - - - - - - - + android:usesCleartextTraffic="true" + > + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/magnetic_grid.json b/app/src/main/assets/magnetic_grid.json new file mode 100644 index 00000000..1a39876e --- /dev/null +++ b/app/src/main/assets/magnetic_grid.json @@ -0,0 +1,22 @@ +{ + "version": 1, + "description": "Magnetic heading compensation grid in local meter coordinates", + "grid": [ + { + "x": 10.0, + "y": 15.0, + "correctionRad": 0, + "confidence": 0.9, + "sampleCount": 25, + "updatedAt": 1774828800000 + }, + { + "x": 15.0, + "y": 15.0, + "correctionRad": 0, + "confidence": 0.8, + "sampleCount": 18, + "updatedAt": 1774828800000 + } + ] +} diff --git a/app/src/main/java/com/openpositioning/PositionMe/Traj.java b/app/src/main/java/com/openpositioning/PositionMe/Traj.java deleted file mode 100644 index 7925fa55..00000000 --- a/app/src/main/java/com/openpositioning/PositionMe/Traj.java +++ /dev/null @@ -1,12664 +0,0 @@ -package com.openpositioning.PositionMe;// Generated by the protocol buffer compiler. DO NOT EDIT! -// source: Cloud/app/src/main/proto/traj.proto - -public final class Traj { - private Traj() {} - public static void registerAllExtensions( - com.google.protobuf.ExtensionRegistryLite registry) { - } - - public static void registerAllExtensions( - com.google.protobuf.ExtensionRegistry registry) { - registerAllExtensions( - (com.google.protobuf.ExtensionRegistryLite) registry); - } - public interface TrajectoryOrBuilder extends - // @@protoc_insertion_point(interface_extends:Trajectory) - com.google.protobuf.MessageOrBuilder { - - /** - * optional string android_version = 1; - */ - String getAndroidVersion(); - /** - * optional string android_version = 1; - */ - com.google.protobuf.ByteString - getAndroidVersionBytes(); - - /** - * repeated .Motion_Sample imu_data = 2; - */ - java.util.List - getImuDataList(); - /** - * repeated .Motion_Sample imu_data = 2; - */ - Motion_Sample getImuData(int index); - /** - * repeated .Motion_Sample imu_data = 2; - */ - int getImuDataCount(); - /** - * repeated .Motion_Sample imu_data = 2; - */ - java.util.List - getImuDataOrBuilderList(); - /** - * repeated .Motion_Sample imu_data = 2; - */ - Motion_SampleOrBuilder getImuDataOrBuilder( - int index); - - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - java.util.List - getPdrDataList(); - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - Pdr_Sample getPdrData(int index); - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - int getPdrDataCount(); - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - java.util.List - getPdrDataOrBuilderList(); - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - Pdr_SampleOrBuilder getPdrDataOrBuilder( - int index); - - /** - * repeated .Position_Sample position_data = 4; - */ - java.util.List - getPositionDataList(); - /** - * repeated .Position_Sample position_data = 4; - */ - Position_Sample getPositionData(int index); - /** - * repeated .Position_Sample position_data = 4; - */ - int getPositionDataCount(); - /** - * repeated .Position_Sample position_data = 4; - */ - java.util.List - getPositionDataOrBuilderList(); - /** - * repeated .Position_Sample position_data = 4; - */ - Position_SampleOrBuilder getPositionDataOrBuilder( - int index); - - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - java.util.List - getPressureDataList(); - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - Pressure_Sample getPressureData(int index); - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - int getPressureDataCount(); - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - java.util.List - getPressureDataOrBuilderList(); - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - Pressure_SampleOrBuilder getPressureDataOrBuilder( - int index); - - /** - * repeated .Light_Sample light_data = 6; - */ - java.util.List - getLightDataList(); - /** - * repeated .Light_Sample light_data = 6; - */ - Light_Sample getLightData(int index); - /** - * repeated .Light_Sample light_data = 6; - */ - int getLightDataCount(); - /** - * repeated .Light_Sample light_data = 6; - */ - java.util.List - getLightDataOrBuilderList(); - /** - * repeated .Light_Sample light_data = 6; - */ - Light_SampleOrBuilder getLightDataOrBuilder( - int index); - - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - java.util.List - getGnssDataList(); - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - GNSS_Sample getGnssData(int index); - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - int getGnssDataCount(); - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - java.util.List - getGnssDataOrBuilderList(); - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - GNSS_SampleOrBuilder getGnssDataOrBuilder( - int index); - - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - java.util.List - getWifiDataList(); - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - WiFi_Sample getWifiData(int index); - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - int getWifiDataCount(); - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - java.util.List - getWifiDataOrBuilderList(); - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - WiFi_SampleOrBuilder getWifiDataOrBuilder( - int index); - - /** - * repeated .AP_Data aps_data = 9; - */ - java.util.List - getApsDataList(); - /** - * repeated .AP_Data aps_data = 9; - */ - AP_Data getApsData(int index); - /** - * repeated .AP_Data aps_data = 9; - */ - int getApsDataCount(); - /** - * repeated .AP_Data aps_data = 9; - */ - java.util.List - getApsDataOrBuilderList(); - /** - * repeated .AP_Data aps_data = 9; - */ - AP_DataOrBuilder getApsDataOrBuilder( - int index); - - /** - *
-     * UNIX timestamp (in milliseconds) recorded from the start of this
-     * trajectory data collection event. All future
-     * timestamps in sub classes are to be RELATIVE timestamps
-     * (in milliseconds) to this start time.
-     * E.g.
-     * start_timestamp = 1674819807315 (UTC 27 Jan 2023 in the morning)
-     * relative_timestamp = 3000 (3s)
-     * 
- * - * optional int64 start_timestamp = 10; - */ - long getStartTimestamp(); - - /** - * optional string data_identifier = 11; - */ - String getDataIdentifier(); - /** - * optional string data_identifier = 11; - */ - com.google.protobuf.ByteString - getDataIdentifierBytes(); - - /** - * optional .Sensor_Info accelerometer_info = 12; - */ - boolean hasAccelerometerInfo(); - /** - * optional .Sensor_Info accelerometer_info = 12; - */ - Sensor_Info getAccelerometerInfo(); - /** - * optional .Sensor_Info accelerometer_info = 12; - */ - Sensor_InfoOrBuilder getAccelerometerInfoOrBuilder(); - - /** - * optional .Sensor_Info gyroscope_info = 13; - */ - boolean hasGyroscopeInfo(); - /** - * optional .Sensor_Info gyroscope_info = 13; - */ - Sensor_Info getGyroscopeInfo(); - /** - * optional .Sensor_Info gyroscope_info = 13; - */ - Sensor_InfoOrBuilder getGyroscopeInfoOrBuilder(); - - /** - * optional .Sensor_Info rotation_vector_info = 14; - */ - boolean hasRotationVectorInfo(); - /** - * optional .Sensor_Info rotation_vector_info = 14; - */ - Sensor_Info getRotationVectorInfo(); - /** - * optional .Sensor_Info rotation_vector_info = 14; - */ - Sensor_InfoOrBuilder getRotationVectorInfoOrBuilder(); - - /** - * optional .Sensor_Info magnetometer_info = 15; - */ - boolean hasMagnetometerInfo(); - /** - * optional .Sensor_Info magnetometer_info = 15; - */ - Sensor_Info getMagnetometerInfo(); - /** - * optional .Sensor_Info magnetometer_info = 15; - */ - Sensor_InfoOrBuilder getMagnetometerInfoOrBuilder(); - - /** - * optional .Sensor_Info barometer_info = 16; - */ - boolean hasBarometerInfo(); - /** - * optional .Sensor_Info barometer_info = 16; - */ - Sensor_Info getBarometerInfo(); - /** - * optional .Sensor_Info barometer_info = 16; - */ - Sensor_InfoOrBuilder getBarometerInfoOrBuilder(); - - /** - * optional .Sensor_Info light_sensor_info = 17; - */ - boolean hasLightSensorInfo(); - /** - * optional .Sensor_Info light_sensor_info = 17; - */ - Sensor_Info getLightSensorInfo(); - /** - * optional .Sensor_Info light_sensor_info = 17; - */ - Sensor_InfoOrBuilder getLightSensorInfoOrBuilder(); - } - /** - * Protobuf type {@code Trajectory} - */ - public static final class Trajectory extends - com.google.protobuf.GeneratedMessageV3 implements - // @@protoc_insertion_point(message_implements:Trajectory) - TrajectoryOrBuilder { - // Use Trajectory.newBuilder() to construct. - private Trajectory(com.google.protobuf.GeneratedMessageV3.Builder builder) { - super(builder); - } - private Trajectory() { - androidVersion_ = ""; - imuData_ = java.util.Collections.emptyList(); - pdrData_ = java.util.Collections.emptyList(); - positionData_ = java.util.Collections.emptyList(); - pressureData_ = java.util.Collections.emptyList(); - lightData_ = java.util.Collections.emptyList(); - gnssData_ = java.util.Collections.emptyList(); - wifiData_ = java.util.Collections.emptyList(); - apsData_ = java.util.Collections.emptyList(); - startTimestamp_ = 0L; - dataIdentifier_ = ""; - } - - @Override - public final com.google.protobuf.UnknownFieldSet - getUnknownFields() { - return com.google.protobuf.UnknownFieldSet.getDefaultInstance(); - } - private Trajectory( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - this(); - int mutable_bitField0_ = 0; - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - default: { - if (!input.skipField(tag)) { - done = true; - } - break; - } - case 10: { - String s = input.readStringRequireUtf8(); - - androidVersion_ = s; - break; - } - case 18: { - if (!((mutable_bitField0_ & 0x00000002) == 0x00000002)) { - imuData_ = new java.util.ArrayList(); - mutable_bitField0_ |= 0x00000002; - } - imuData_.add( - input.readMessage(Motion_Sample.parser(), extensionRegistry)); - break; - } - case 26: { - if (!((mutable_bitField0_ & 0x00000004) == 0x00000004)) { - pdrData_ = new java.util.ArrayList(); - mutable_bitField0_ |= 0x00000004; - } - pdrData_.add( - input.readMessage(Pdr_Sample.parser(), extensionRegistry)); - break; - } - case 34: { - if (!((mutable_bitField0_ & 0x00000008) == 0x00000008)) { - positionData_ = new java.util.ArrayList(); - mutable_bitField0_ |= 0x00000008; - } - positionData_.add( - input.readMessage(Position_Sample.parser(), extensionRegistry)); - break; - } - case 42: { - if (!((mutable_bitField0_ & 0x00000010) == 0x00000010)) { - pressureData_ = new java.util.ArrayList(); - mutable_bitField0_ |= 0x00000010; - } - pressureData_.add( - input.readMessage(Pressure_Sample.parser(), extensionRegistry)); - break; - } - case 50: { - if (!((mutable_bitField0_ & 0x00000020) == 0x00000020)) { - lightData_ = new java.util.ArrayList(); - mutable_bitField0_ |= 0x00000020; - } - lightData_.add( - input.readMessage(Light_Sample.parser(), extensionRegistry)); - break; - } - case 58: { - if (!((mutable_bitField0_ & 0x00000040) == 0x00000040)) { - gnssData_ = new java.util.ArrayList(); - mutable_bitField0_ |= 0x00000040; - } - gnssData_.add( - input.readMessage(GNSS_Sample.parser(), extensionRegistry)); - break; - } - case 66: { - if (!((mutable_bitField0_ & 0x00000080) == 0x00000080)) { - wifiData_ = new java.util.ArrayList(); - mutable_bitField0_ |= 0x00000080; - } - wifiData_.add( - input.readMessage(WiFi_Sample.parser(), extensionRegistry)); - break; - } - case 74: { - if (!((mutable_bitField0_ & 0x00000100) == 0x00000100)) { - apsData_ = new java.util.ArrayList(); - mutable_bitField0_ |= 0x00000100; - } - apsData_.add( - input.readMessage(AP_Data.parser(), extensionRegistry)); - break; - } - case 80: { - - startTimestamp_ = input.readInt64(); - break; - } - case 90: { - String s = input.readStringRequireUtf8(); - - dataIdentifier_ = s; - break; - } - case 98: { - Sensor_Info.Builder subBuilder = null; - if (accelerometerInfo_ != null) { - subBuilder = accelerometerInfo_.toBuilder(); - } - accelerometerInfo_ = input.readMessage(Sensor_Info.parser(), extensionRegistry); - if (subBuilder != null) { - subBuilder.mergeFrom(accelerometerInfo_); - accelerometerInfo_ = subBuilder.buildPartial(); - } - - break; - } - case 106: { - Sensor_Info.Builder subBuilder = null; - if (gyroscopeInfo_ != null) { - subBuilder = gyroscopeInfo_.toBuilder(); - } - gyroscopeInfo_ = input.readMessage(Sensor_Info.parser(), extensionRegistry); - if (subBuilder != null) { - subBuilder.mergeFrom(gyroscopeInfo_); - gyroscopeInfo_ = subBuilder.buildPartial(); - } - - break; - } - case 114: { - Sensor_Info.Builder subBuilder = null; - if (rotationVectorInfo_ != null) { - subBuilder = rotationVectorInfo_.toBuilder(); - } - rotationVectorInfo_ = input.readMessage(Sensor_Info.parser(), extensionRegistry); - if (subBuilder != null) { - subBuilder.mergeFrom(rotationVectorInfo_); - rotationVectorInfo_ = subBuilder.buildPartial(); - } - - break; - } - case 122: { - Sensor_Info.Builder subBuilder = null; - if (magnetometerInfo_ != null) { - subBuilder = magnetometerInfo_.toBuilder(); - } - magnetometerInfo_ = input.readMessage(Sensor_Info.parser(), extensionRegistry); - if (subBuilder != null) { - subBuilder.mergeFrom(magnetometerInfo_); - magnetometerInfo_ = subBuilder.buildPartial(); - } - - break; - } - case 130: { - Sensor_Info.Builder subBuilder = null; - if (barometerInfo_ != null) { - subBuilder = barometerInfo_.toBuilder(); - } - barometerInfo_ = input.readMessage(Sensor_Info.parser(), extensionRegistry); - if (subBuilder != null) { - subBuilder.mergeFrom(barometerInfo_); - barometerInfo_ = subBuilder.buildPartial(); - } - - break; - } - case 138: { - Sensor_Info.Builder subBuilder = null; - if (lightSensorInfo_ != null) { - subBuilder = lightSensorInfo_.toBuilder(); - } - lightSensorInfo_ = input.readMessage(Sensor_Info.parser(), extensionRegistry); - if (subBuilder != null) { - subBuilder.mergeFrom(lightSensorInfo_); - lightSensorInfo_ = subBuilder.buildPartial(); - } - - break; - } - } - } - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(this); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException( - e).setUnfinishedMessage(this); - } finally { - if (((mutable_bitField0_ & 0x00000002) == 0x00000002)) { - imuData_ = java.util.Collections.unmodifiableList(imuData_); - } - if (((mutable_bitField0_ & 0x00000004) == 0x00000004)) { - pdrData_ = java.util.Collections.unmodifiableList(pdrData_); - } - if (((mutable_bitField0_ & 0x00000008) == 0x00000008)) { - positionData_ = java.util.Collections.unmodifiableList(positionData_); - } - if (((mutable_bitField0_ & 0x00000010) == 0x00000010)) { - pressureData_ = java.util.Collections.unmodifiableList(pressureData_); - } - if (((mutable_bitField0_ & 0x00000020) == 0x00000020)) { - lightData_ = java.util.Collections.unmodifiableList(lightData_); - } - if (((mutable_bitField0_ & 0x00000040) == 0x00000040)) { - gnssData_ = java.util.Collections.unmodifiableList(gnssData_); - } - if (((mutable_bitField0_ & 0x00000080) == 0x00000080)) { - wifiData_ = java.util.Collections.unmodifiableList(wifiData_); - } - if (((mutable_bitField0_ & 0x00000100) == 0x00000100)) { - apsData_ = java.util.Collections.unmodifiableList(apsData_); - } - makeExtensionsImmutable(); - } - } - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return Traj.internal_static_Trajectory_descriptor; - } - - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return Traj.internal_static_Trajectory_fieldAccessorTable - .ensureFieldAccessorsInitialized( - Trajectory.class, Builder.class); - } - - private int bitField0_; - public static final int ANDROID_VERSION_FIELD_NUMBER = 1; - private volatile Object androidVersion_; - /** - * optional string android_version = 1; - */ - public String getAndroidVersion() { - Object ref = androidVersion_; - if (ref instanceof String) { - return (String) ref; - } else { - com.google.protobuf.ByteString bs = - (com.google.protobuf.ByteString) ref; - String s = bs.toStringUtf8(); - androidVersion_ = s; - return s; - } - } - /** - * optional string android_version = 1; - */ - public com.google.protobuf.ByteString - getAndroidVersionBytes() { - Object ref = androidVersion_; - if (ref instanceof String) { - com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8( - (String) ref); - androidVersion_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - - public static final int IMU_DATA_FIELD_NUMBER = 2; - private java.util.List imuData_; - /** - * repeated .Motion_Sample imu_data = 2; - */ - public java.util.List getImuDataList() { - return imuData_; - } - /** - * repeated .Motion_Sample imu_data = 2; - */ - public java.util.List - getImuDataOrBuilderList() { - return imuData_; - } - /** - * repeated .Motion_Sample imu_data = 2; - */ - public int getImuDataCount() { - return imuData_.size(); - } - /** - * repeated .Motion_Sample imu_data = 2; - */ - public Motion_Sample getImuData(int index) { - return imuData_.get(index); - } - /** - * repeated .Motion_Sample imu_data = 2; - */ - public Motion_SampleOrBuilder getImuDataOrBuilder( - int index) { - return imuData_.get(index); - } - - public static final int PDR_DATA_FIELD_NUMBER = 3; - private java.util.List pdrData_; - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public java.util.List getPdrDataList() { - return pdrData_; - } - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public java.util.List - getPdrDataOrBuilderList() { - return pdrData_; - } - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public int getPdrDataCount() { - return pdrData_.size(); - } - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public Pdr_Sample getPdrData(int index) { - return pdrData_.get(index); - } - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public Pdr_SampleOrBuilder getPdrDataOrBuilder( - int index) { - return pdrData_.get(index); - } - - public static final int POSITION_DATA_FIELD_NUMBER = 4; - private java.util.List positionData_; - /** - * repeated .Position_Sample position_data = 4; - */ - public java.util.List getPositionDataList() { - return positionData_; - } - /** - * repeated .Position_Sample position_data = 4; - */ - public java.util.List - getPositionDataOrBuilderList() { - return positionData_; - } - /** - * repeated .Position_Sample position_data = 4; - */ - public int getPositionDataCount() { - return positionData_.size(); - } - /** - * repeated .Position_Sample position_data = 4; - */ - public Position_Sample getPositionData(int index) { - return positionData_.get(index); - } - /** - * repeated .Position_Sample position_data = 4; - */ - public Position_SampleOrBuilder getPositionDataOrBuilder( - int index) { - return positionData_.get(index); - } - - public static final int PRESSURE_DATA_FIELD_NUMBER = 5; - private java.util.List pressureData_; - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public java.util.List getPressureDataList() { - return pressureData_; - } - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public java.util.List - getPressureDataOrBuilderList() { - return pressureData_; - } - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public int getPressureDataCount() { - return pressureData_.size(); - } - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public Pressure_Sample getPressureData(int index) { - return pressureData_.get(index); - } - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public Pressure_SampleOrBuilder getPressureDataOrBuilder( - int index) { - return pressureData_.get(index); - } - - public static final int LIGHT_DATA_FIELD_NUMBER = 6; - private java.util.List lightData_; - /** - * repeated .Light_Sample light_data = 6; - */ - public java.util.List getLightDataList() { - return lightData_; - } - /** - * repeated .Light_Sample light_data = 6; - */ - public java.util.List - getLightDataOrBuilderList() { - return lightData_; - } - /** - * repeated .Light_Sample light_data = 6; - */ - public int getLightDataCount() { - return lightData_.size(); - } - /** - * repeated .Light_Sample light_data = 6; - */ - public Light_Sample getLightData(int index) { - return lightData_.get(index); - } - /** - * repeated .Light_Sample light_data = 6; - */ - public Light_SampleOrBuilder getLightDataOrBuilder( - int index) { - return lightData_.get(index); - } - - public static final int GNSS_DATA_FIELD_NUMBER = 7; - private java.util.List gnssData_; - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public java.util.List getGnssDataList() { - return gnssData_; - } - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public java.util.List - getGnssDataOrBuilderList() { - return gnssData_; - } - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public int getGnssDataCount() { - return gnssData_.size(); - } - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public GNSS_Sample getGnssData(int index) { - return gnssData_.get(index); - } - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public GNSS_SampleOrBuilder getGnssDataOrBuilder( - int index) { - return gnssData_.get(index); - } - - public static final int WIFI_DATA_FIELD_NUMBER = 8; - private java.util.List wifiData_; - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public java.util.List getWifiDataList() { - return wifiData_; - } - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public java.util.List - getWifiDataOrBuilderList() { - return wifiData_; - } - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public int getWifiDataCount() { - return wifiData_.size(); - } - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public WiFi_Sample getWifiData(int index) { - return wifiData_.get(index); - } - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public WiFi_SampleOrBuilder getWifiDataOrBuilder( - int index) { - return wifiData_.get(index); - } - - public static final int APS_DATA_FIELD_NUMBER = 9; - private java.util.List apsData_; - /** - * repeated .AP_Data aps_data = 9; - */ - public java.util.List getApsDataList() { - return apsData_; - } - /** - * repeated .AP_Data aps_data = 9; - */ - public java.util.List - getApsDataOrBuilderList() { - return apsData_; - } - /** - * repeated .AP_Data aps_data = 9; - */ - public int getApsDataCount() { - return apsData_.size(); - } - /** - * repeated .AP_Data aps_data = 9; - */ - public AP_Data getApsData(int index) { - return apsData_.get(index); - } - /** - * repeated .AP_Data aps_data = 9; - */ - public AP_DataOrBuilder getApsDataOrBuilder( - int index) { - return apsData_.get(index); - } - - public static final int START_TIMESTAMP_FIELD_NUMBER = 10; - private long startTimestamp_; - /** - *
-     * UNIX timestamp (in milliseconds) recorded from the start of this
-     * trajectory data collection event. All future
-     * timestamps in sub classes are to be RELATIVE timestamps
-     * (in milliseconds) to this start time.
-     * E.g.
-     * start_timestamp = 1674819807315 (UTC 27 Jan 2023 in the morning)
-     * relative_timestamp = 3000 (3s)
-     * 
- * - * optional int64 start_timestamp = 10; - */ - public long getStartTimestamp() { - return startTimestamp_; - } - - public static final int DATA_IDENTIFIER_FIELD_NUMBER = 11; - private volatile Object dataIdentifier_; - /** - * optional string data_identifier = 11; - */ - public String getDataIdentifier() { - Object ref = dataIdentifier_; - if (ref instanceof String) { - return (String) ref; - } else { - com.google.protobuf.ByteString bs = - (com.google.protobuf.ByteString) ref; - String s = bs.toStringUtf8(); - dataIdentifier_ = s; - return s; - } - } - /** - * optional string data_identifier = 11; - */ - public com.google.protobuf.ByteString - getDataIdentifierBytes() { - Object ref = dataIdentifier_; - if (ref instanceof String) { - com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8( - (String) ref); - dataIdentifier_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - - public static final int ACCELEROMETER_INFO_FIELD_NUMBER = 12; - private Sensor_Info accelerometerInfo_; - /** - * optional .Sensor_Info accelerometer_info = 12; - */ - public boolean hasAccelerometerInfo() { - return accelerometerInfo_ != null; - } - /** - * optional .Sensor_Info accelerometer_info = 12; - */ - public Sensor_Info getAccelerometerInfo() { - return accelerometerInfo_ == null ? Sensor_Info.getDefaultInstance() : accelerometerInfo_; - } - /** - * optional .Sensor_Info accelerometer_info = 12; - */ - public Sensor_InfoOrBuilder getAccelerometerInfoOrBuilder() { - return getAccelerometerInfo(); - } - - public static final int GYROSCOPE_INFO_FIELD_NUMBER = 13; - private Sensor_Info gyroscopeInfo_; - /** - * optional .Sensor_Info gyroscope_info = 13; - */ - public boolean hasGyroscopeInfo() { - return gyroscopeInfo_ != null; - } - /** - * optional .Sensor_Info gyroscope_info = 13; - */ - public Sensor_Info getGyroscopeInfo() { - return gyroscopeInfo_ == null ? Sensor_Info.getDefaultInstance() : gyroscopeInfo_; - } - /** - * optional .Sensor_Info gyroscope_info = 13; - */ - public Sensor_InfoOrBuilder getGyroscopeInfoOrBuilder() { - return getGyroscopeInfo(); - } - - public static final int ROTATION_VECTOR_INFO_FIELD_NUMBER = 14; - private Sensor_Info rotationVectorInfo_; - /** - * optional .Sensor_Info rotation_vector_info = 14; - */ - public boolean hasRotationVectorInfo() { - return rotationVectorInfo_ != null; - } - /** - * optional .Sensor_Info rotation_vector_info = 14; - */ - public Sensor_Info getRotationVectorInfo() { - return rotationVectorInfo_ == null ? Sensor_Info.getDefaultInstance() : rotationVectorInfo_; - } - /** - * optional .Sensor_Info rotation_vector_info = 14; - */ - public Sensor_InfoOrBuilder getRotationVectorInfoOrBuilder() { - return getRotationVectorInfo(); - } - - public static final int MAGNETOMETER_INFO_FIELD_NUMBER = 15; - private Sensor_Info magnetometerInfo_; - /** - * optional .Sensor_Info magnetometer_info = 15; - */ - public boolean hasMagnetometerInfo() { - return magnetometerInfo_ != null; - } - /** - * optional .Sensor_Info magnetometer_info = 15; - */ - public Sensor_Info getMagnetometerInfo() { - return magnetometerInfo_ == null ? Sensor_Info.getDefaultInstance() : magnetometerInfo_; - } - /** - * optional .Sensor_Info magnetometer_info = 15; - */ - public Sensor_InfoOrBuilder getMagnetometerInfoOrBuilder() { - return getMagnetometerInfo(); - } - - public static final int BAROMETER_INFO_FIELD_NUMBER = 16; - private Sensor_Info barometerInfo_; - /** - * optional .Sensor_Info barometer_info = 16; - */ - public boolean hasBarometerInfo() { - return barometerInfo_ != null; - } - /** - * optional .Sensor_Info barometer_info = 16; - */ - public Sensor_Info getBarometerInfo() { - return barometerInfo_ == null ? Sensor_Info.getDefaultInstance() : barometerInfo_; - } - /** - * optional .Sensor_Info barometer_info = 16; - */ - public Sensor_InfoOrBuilder getBarometerInfoOrBuilder() { - return getBarometerInfo(); - } - - public static final int LIGHT_SENSOR_INFO_FIELD_NUMBER = 17; - private Sensor_Info lightSensorInfo_; - /** - * optional .Sensor_Info light_sensor_info = 17; - */ - public boolean hasLightSensorInfo() { - return lightSensorInfo_ != null; - } - /** - * optional .Sensor_Info light_sensor_info = 17; - */ - public Sensor_Info getLightSensorInfo() { - return lightSensorInfo_ == null ? Sensor_Info.getDefaultInstance() : lightSensorInfo_; - } - /** - * optional .Sensor_Info light_sensor_info = 17; - */ - public Sensor_InfoOrBuilder getLightSensorInfoOrBuilder() { - return getLightSensorInfo(); - } - - private byte memoizedIsInitialized = -1; - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) return true; - if (isInitialized == 0) return false; - - memoizedIsInitialized = 1; - return true; - } - - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - if (!getAndroidVersionBytes().isEmpty()) { - com.google.protobuf.GeneratedMessageV3.writeString(output, 1, androidVersion_); - } - for (int i = 0; i < imuData_.size(); i++) { - output.writeMessage(2, imuData_.get(i)); - } - for (int i = 0; i < pdrData_.size(); i++) { - output.writeMessage(3, pdrData_.get(i)); - } - for (int i = 0; i < positionData_.size(); i++) { - output.writeMessage(4, positionData_.get(i)); - } - for (int i = 0; i < pressureData_.size(); i++) { - output.writeMessage(5, pressureData_.get(i)); - } - for (int i = 0; i < lightData_.size(); i++) { - output.writeMessage(6, lightData_.get(i)); - } - for (int i = 0; i < gnssData_.size(); i++) { - output.writeMessage(7, gnssData_.get(i)); - } - for (int i = 0; i < wifiData_.size(); i++) { - output.writeMessage(8, wifiData_.get(i)); - } - for (int i = 0; i < apsData_.size(); i++) { - output.writeMessage(9, apsData_.get(i)); - } - if (startTimestamp_ != 0L) { - output.writeInt64(10, startTimestamp_); - } - if (!getDataIdentifierBytes().isEmpty()) { - com.google.protobuf.GeneratedMessageV3.writeString(output, 11, dataIdentifier_); - } - if (accelerometerInfo_ != null) { - output.writeMessage(12, getAccelerometerInfo()); - } - if (gyroscopeInfo_ != null) { - output.writeMessage(13, getGyroscopeInfo()); - } - if (rotationVectorInfo_ != null) { - output.writeMessage(14, getRotationVectorInfo()); - } - if (magnetometerInfo_ != null) { - output.writeMessage(15, getMagnetometerInfo()); - } - if (barometerInfo_ != null) { - output.writeMessage(16, getBarometerInfo()); - } - if (lightSensorInfo_ != null) { - output.writeMessage(17, getLightSensorInfo()); - } - } - - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) return size; - - size = 0; - if (!getAndroidVersionBytes().isEmpty()) { - size += com.google.protobuf.GeneratedMessageV3.computeStringSize(1, androidVersion_); - } - for (int i = 0; i < imuData_.size(); i++) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize(2, imuData_.get(i)); - } - for (int i = 0; i < pdrData_.size(); i++) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize(3, pdrData_.get(i)); - } - for (int i = 0; i < positionData_.size(); i++) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize(4, positionData_.get(i)); - } - for (int i = 0; i < pressureData_.size(); i++) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize(5, pressureData_.get(i)); - } - for (int i = 0; i < lightData_.size(); i++) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize(6, lightData_.get(i)); - } - for (int i = 0; i < gnssData_.size(); i++) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize(7, gnssData_.get(i)); - } - for (int i = 0; i < wifiData_.size(); i++) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize(8, wifiData_.get(i)); - } - for (int i = 0; i < apsData_.size(); i++) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize(9, apsData_.get(i)); - } - if (startTimestamp_ != 0L) { - size += com.google.protobuf.CodedOutputStream - .computeInt64Size(10, startTimestamp_); - } - if (!getDataIdentifierBytes().isEmpty()) { - size += com.google.protobuf.GeneratedMessageV3.computeStringSize(11, dataIdentifier_); - } - if (accelerometerInfo_ != null) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize(12, getAccelerometerInfo()); - } - if (gyroscopeInfo_ != null) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize(13, getGyroscopeInfo()); - } - if (rotationVectorInfo_ != null) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize(14, getRotationVectorInfo()); - } - if (magnetometerInfo_ != null) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize(15, getMagnetometerInfo()); - } - if (barometerInfo_ != null) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize(16, getBarometerInfo()); - } - if (lightSensorInfo_ != null) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize(17, getLightSensorInfo()); - } - memoizedSize = size; - return size; - } - - private static final long serialVersionUID = 0L; - @Override - public boolean equals(final Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof Trajectory)) { - return super.equals(obj); - } - Trajectory other = (Trajectory) obj; - - boolean result = true; - result = result && getAndroidVersion() - .equals(other.getAndroidVersion()); - result = result && getImuDataList() - .equals(other.getImuDataList()); - result = result && getPdrDataList() - .equals(other.getPdrDataList()); - result = result && getPositionDataList() - .equals(other.getPositionDataList()); - result = result && getPressureDataList() - .equals(other.getPressureDataList()); - result = result && getLightDataList() - .equals(other.getLightDataList()); - result = result && getGnssDataList() - .equals(other.getGnssDataList()); - result = result && getWifiDataList() - .equals(other.getWifiDataList()); - result = result && getApsDataList() - .equals(other.getApsDataList()); - result = result && (getStartTimestamp() - == other.getStartTimestamp()); - result = result && getDataIdentifier() - .equals(other.getDataIdentifier()); - result = result && (hasAccelerometerInfo() == other.hasAccelerometerInfo()); - if (hasAccelerometerInfo()) { - result = result && getAccelerometerInfo() - .equals(other.getAccelerometerInfo()); - } - result = result && (hasGyroscopeInfo() == other.hasGyroscopeInfo()); - if (hasGyroscopeInfo()) { - result = result && getGyroscopeInfo() - .equals(other.getGyroscopeInfo()); - } - result = result && (hasRotationVectorInfo() == other.hasRotationVectorInfo()); - if (hasRotationVectorInfo()) { - result = result && getRotationVectorInfo() - .equals(other.getRotationVectorInfo()); - } - result = result && (hasMagnetometerInfo() == other.hasMagnetometerInfo()); - if (hasMagnetometerInfo()) { - result = result && getMagnetometerInfo() - .equals(other.getMagnetometerInfo()); - } - result = result && (hasBarometerInfo() == other.hasBarometerInfo()); - if (hasBarometerInfo()) { - result = result && getBarometerInfo() - .equals(other.getBarometerInfo()); - } - result = result && (hasLightSensorInfo() == other.hasLightSensorInfo()); - if (hasLightSensorInfo()) { - result = result && getLightSensorInfo() - .equals(other.getLightSensorInfo()); - } - return result; - } - - @Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptorForType().hashCode(); - hash = (37 * hash) + ANDROID_VERSION_FIELD_NUMBER; - hash = (53 * hash) + getAndroidVersion().hashCode(); - if (getImuDataCount() > 0) { - hash = (37 * hash) + IMU_DATA_FIELD_NUMBER; - hash = (53 * hash) + getImuDataList().hashCode(); - } - if (getPdrDataCount() > 0) { - hash = (37 * hash) + PDR_DATA_FIELD_NUMBER; - hash = (53 * hash) + getPdrDataList().hashCode(); - } - if (getPositionDataCount() > 0) { - hash = (37 * hash) + POSITION_DATA_FIELD_NUMBER; - hash = (53 * hash) + getPositionDataList().hashCode(); - } - if (getPressureDataCount() > 0) { - hash = (37 * hash) + PRESSURE_DATA_FIELD_NUMBER; - hash = (53 * hash) + getPressureDataList().hashCode(); - } - if (getLightDataCount() > 0) { - hash = (37 * hash) + LIGHT_DATA_FIELD_NUMBER; - hash = (53 * hash) + getLightDataList().hashCode(); - } - if (getGnssDataCount() > 0) { - hash = (37 * hash) + GNSS_DATA_FIELD_NUMBER; - hash = (53 * hash) + getGnssDataList().hashCode(); - } - if (getWifiDataCount() > 0) { - hash = (37 * hash) + WIFI_DATA_FIELD_NUMBER; - hash = (53 * hash) + getWifiDataList().hashCode(); - } - if (getApsDataCount() > 0) { - hash = (37 * hash) + APS_DATA_FIELD_NUMBER; - hash = (53 * hash) + getApsDataList().hashCode(); - } - hash = (37 * hash) + START_TIMESTAMP_FIELD_NUMBER; - hash = (53 * hash) + com.google.protobuf.Internal.hashLong( - getStartTimestamp()); - hash = (37 * hash) + DATA_IDENTIFIER_FIELD_NUMBER; - hash = (53 * hash) + getDataIdentifier().hashCode(); - if (hasAccelerometerInfo()) { - hash = (37 * hash) + ACCELEROMETER_INFO_FIELD_NUMBER; - hash = (53 * hash) + getAccelerometerInfo().hashCode(); - } - if (hasGyroscopeInfo()) { - hash = (37 * hash) + GYROSCOPE_INFO_FIELD_NUMBER; - hash = (53 * hash) + getGyroscopeInfo().hashCode(); - } - if (hasRotationVectorInfo()) { - hash = (37 * hash) + ROTATION_VECTOR_INFO_FIELD_NUMBER; - hash = (53 * hash) + getRotationVectorInfo().hashCode(); - } - if (hasMagnetometerInfo()) { - hash = (37 * hash) + MAGNETOMETER_INFO_FIELD_NUMBER; - hash = (53 * hash) + getMagnetometerInfo().hashCode(); - } - if (hasBarometerInfo()) { - hash = (37 * hash) + BAROMETER_INFO_FIELD_NUMBER; - hash = (53 * hash) + getBarometerInfo().hashCode(); - } - if (hasLightSensorInfo()) { - hash = (37 * hash) + LIGHT_SENSOR_INFO_FIELD_NUMBER; - hash = (53 * hash) + getLightSensorInfo().hashCode(); - } - hash = (29 * hash) + unknownFields.hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static Trajectory parseFrom( - com.google.protobuf.ByteString data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static Trajectory parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static Trajectory parseFrom(byte[] data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static Trajectory parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static Trajectory parseFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input); - } - public static Trajectory parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input, extensionRegistry); - } - public static Trajectory parseDelimitedFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseDelimitedWithIOException(PARSER, input); - } - public static Trajectory parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - public static Trajectory parseFrom( - com.google.protobuf.CodedInputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input); - } - public static Trajectory parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public Builder newBuilderForType() { return newBuilder(); } - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - public static Builder newBuilder(Trajectory prototype) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() : new Builder().mergeFrom(this); - } - - @Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { - Builder builder = new Builder(parent); - return builder; - } - /** - * Protobuf type {@code Trajectory} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessageV3.Builder implements - // @@protoc_insertion_point(builder_implements:Trajectory) - TrajectoryOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return Traj.internal_static_Trajectory_descriptor; - } - - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return Traj.internal_static_Trajectory_fieldAccessorTable - .ensureFieldAccessorsInitialized( - Trajectory.class, Builder.class); - } - - // Construct using Traj.Trajectory.newBuilder() - private Builder() { - maybeForceBuilderInitialization(); - } - - private Builder( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { - super(parent); - maybeForceBuilderInitialization(); - } - private void maybeForceBuilderInitialization() { - if (com.google.protobuf.GeneratedMessageV3 - .alwaysUseFieldBuilders) { - getImuDataFieldBuilder(); - getPdrDataFieldBuilder(); - getPositionDataFieldBuilder(); - getPressureDataFieldBuilder(); - getLightDataFieldBuilder(); - getGnssDataFieldBuilder(); - getWifiDataFieldBuilder(); - getApsDataFieldBuilder(); - } - } - public Builder clear() { - super.clear(); - androidVersion_ = ""; - - if (imuDataBuilder_ == null) { - imuData_ = java.util.Collections.emptyList(); - bitField0_ = (bitField0_ & ~0x00000002); - } else { - imuDataBuilder_.clear(); - } - if (pdrDataBuilder_ == null) { - pdrData_ = java.util.Collections.emptyList(); - bitField0_ = (bitField0_ & ~0x00000004); - } else { - pdrDataBuilder_.clear(); - } - if (positionDataBuilder_ == null) { - positionData_ = java.util.Collections.emptyList(); - bitField0_ = (bitField0_ & ~0x00000008); - } else { - positionDataBuilder_.clear(); - } - if (pressureDataBuilder_ == null) { - pressureData_ = java.util.Collections.emptyList(); - bitField0_ = (bitField0_ & ~0x00000010); - } else { - pressureDataBuilder_.clear(); - } - if (lightDataBuilder_ == null) { - lightData_ = java.util.Collections.emptyList(); - bitField0_ = (bitField0_ & ~0x00000020); - } else { - lightDataBuilder_.clear(); - } - if (gnssDataBuilder_ == null) { - gnssData_ = java.util.Collections.emptyList(); - bitField0_ = (bitField0_ & ~0x00000040); - } else { - gnssDataBuilder_.clear(); - } - if (wifiDataBuilder_ == null) { - wifiData_ = java.util.Collections.emptyList(); - bitField0_ = (bitField0_ & ~0x00000080); - } else { - wifiDataBuilder_.clear(); - } - if (apsDataBuilder_ == null) { - apsData_ = java.util.Collections.emptyList(); - bitField0_ = (bitField0_ & ~0x00000100); - } else { - apsDataBuilder_.clear(); - } - startTimestamp_ = 0L; - - dataIdentifier_ = ""; - - if (accelerometerInfoBuilder_ == null) { - accelerometerInfo_ = null; - } else { - accelerometerInfo_ = null; - accelerometerInfoBuilder_ = null; - } - if (gyroscopeInfoBuilder_ == null) { - gyroscopeInfo_ = null; - } else { - gyroscopeInfo_ = null; - gyroscopeInfoBuilder_ = null; - } - if (rotationVectorInfoBuilder_ == null) { - rotationVectorInfo_ = null; - } else { - rotationVectorInfo_ = null; - rotationVectorInfoBuilder_ = null; - } - if (magnetometerInfoBuilder_ == null) { - magnetometerInfo_ = null; - } else { - magnetometerInfo_ = null; - magnetometerInfoBuilder_ = null; - } - if (barometerInfoBuilder_ == null) { - barometerInfo_ = null; - } else { - barometerInfo_ = null; - barometerInfoBuilder_ = null; - } - if (lightSensorInfoBuilder_ == null) { - lightSensorInfo_ = null; - } else { - lightSensorInfo_ = null; - lightSensorInfoBuilder_ = null; - } - return this; - } - - public com.google.protobuf.Descriptors.Descriptor - getDescriptorForType() { - return Traj.internal_static_Trajectory_descriptor; - } - - public Trajectory getDefaultInstanceForType() { - return Trajectory.getDefaultInstance(); - } - - public Trajectory build() { - Trajectory result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - public Trajectory buildPartial() { - Trajectory result = new Trajectory(this); - int from_bitField0_ = bitField0_; - int to_bitField0_ = 0; - result.androidVersion_ = androidVersion_; - if (imuDataBuilder_ == null) { - if (((bitField0_ & 0x00000002) == 0x00000002)) { - imuData_ = java.util.Collections.unmodifiableList(imuData_); - bitField0_ = (bitField0_ & ~0x00000002); - } - result.imuData_ = imuData_; - } else { - result.imuData_ = imuDataBuilder_.build(); - } - if (pdrDataBuilder_ == null) { - if (((bitField0_ & 0x00000004) == 0x00000004)) { - pdrData_ = java.util.Collections.unmodifiableList(pdrData_); - bitField0_ = (bitField0_ & ~0x00000004); - } - result.pdrData_ = pdrData_; - } else { - result.pdrData_ = pdrDataBuilder_.build(); - } - if (positionDataBuilder_ == null) { - if (((bitField0_ & 0x00000008) == 0x00000008)) { - positionData_ = java.util.Collections.unmodifiableList(positionData_); - bitField0_ = (bitField0_ & ~0x00000008); - } - result.positionData_ = positionData_; - } else { - result.positionData_ = positionDataBuilder_.build(); - } - if (pressureDataBuilder_ == null) { - if (((bitField0_ & 0x00000010) == 0x00000010)) { - pressureData_ = java.util.Collections.unmodifiableList(pressureData_); - bitField0_ = (bitField0_ & ~0x00000010); - } - result.pressureData_ = pressureData_; - } else { - result.pressureData_ = pressureDataBuilder_.build(); - } - if (lightDataBuilder_ == null) { - if (((bitField0_ & 0x00000020) == 0x00000020)) { - lightData_ = java.util.Collections.unmodifiableList(lightData_); - bitField0_ = (bitField0_ & ~0x00000020); - } - result.lightData_ = lightData_; - } else { - result.lightData_ = lightDataBuilder_.build(); - } - if (gnssDataBuilder_ == null) { - if (((bitField0_ & 0x00000040) == 0x00000040)) { - gnssData_ = java.util.Collections.unmodifiableList(gnssData_); - bitField0_ = (bitField0_ & ~0x00000040); - } - result.gnssData_ = gnssData_; - } else { - result.gnssData_ = gnssDataBuilder_.build(); - } - if (wifiDataBuilder_ == null) { - if (((bitField0_ & 0x00000080) == 0x00000080)) { - wifiData_ = java.util.Collections.unmodifiableList(wifiData_); - bitField0_ = (bitField0_ & ~0x00000080); - } - result.wifiData_ = wifiData_; - } else { - result.wifiData_ = wifiDataBuilder_.build(); - } - if (apsDataBuilder_ == null) { - if (((bitField0_ & 0x00000100) == 0x00000100)) { - apsData_ = java.util.Collections.unmodifiableList(apsData_); - bitField0_ = (bitField0_ & ~0x00000100); - } - result.apsData_ = apsData_; - } else { - result.apsData_ = apsDataBuilder_.build(); - } - result.startTimestamp_ = startTimestamp_; - result.dataIdentifier_ = dataIdentifier_; - if (accelerometerInfoBuilder_ == null) { - result.accelerometerInfo_ = accelerometerInfo_; - } else { - result.accelerometerInfo_ = accelerometerInfoBuilder_.build(); - } - if (gyroscopeInfoBuilder_ == null) { - result.gyroscopeInfo_ = gyroscopeInfo_; - } else { - result.gyroscopeInfo_ = gyroscopeInfoBuilder_.build(); - } - if (rotationVectorInfoBuilder_ == null) { - result.rotationVectorInfo_ = rotationVectorInfo_; - } else { - result.rotationVectorInfo_ = rotationVectorInfoBuilder_.build(); - } - if (magnetometerInfoBuilder_ == null) { - result.magnetometerInfo_ = magnetometerInfo_; - } else { - result.magnetometerInfo_ = magnetometerInfoBuilder_.build(); - } - if (barometerInfoBuilder_ == null) { - result.barometerInfo_ = barometerInfo_; - } else { - result.barometerInfo_ = barometerInfoBuilder_.build(); - } - if (lightSensorInfoBuilder_ == null) { - result.lightSensorInfo_ = lightSensorInfo_; - } else { - result.lightSensorInfo_ = lightSensorInfoBuilder_.build(); - } - result.bitField0_ = to_bitField0_; - onBuilt(); - return result; - } - - public Builder clone() { - return (Builder) super.clone(); - } - public Builder setField( - com.google.protobuf.Descriptors.FieldDescriptor field, - Object value) { - return (Builder) super.setField(field, value); - } - public Builder clearField( - com.google.protobuf.Descriptors.FieldDescriptor field) { - return (Builder) super.clearField(field); - } - public Builder clearOneof( - com.google.protobuf.Descriptors.OneofDescriptor oneof) { - return (Builder) super.clearOneof(oneof); - } - public Builder setRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - int index, Object value) { - return (Builder) super.setRepeatedField(field, index, value); - } - public Builder addRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - Object value) { - return (Builder) super.addRepeatedField(field, value); - } - public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof Trajectory) { - return mergeFrom((Trajectory)other); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom(Trajectory other) { - if (other == Trajectory.getDefaultInstance()) return this; - if (!other.getAndroidVersion().isEmpty()) { - androidVersion_ = other.androidVersion_; - onChanged(); - } - if (imuDataBuilder_ == null) { - if (!other.imuData_.isEmpty()) { - if (imuData_.isEmpty()) { - imuData_ = other.imuData_; - bitField0_ = (bitField0_ & ~0x00000002); - } else { - ensureImuDataIsMutable(); - imuData_.addAll(other.imuData_); - } - onChanged(); - } - } else { - if (!other.imuData_.isEmpty()) { - if (imuDataBuilder_.isEmpty()) { - imuDataBuilder_.dispose(); - imuDataBuilder_ = null; - imuData_ = other.imuData_; - bitField0_ = (bitField0_ & ~0x00000002); - imuDataBuilder_ = - com.google.protobuf.GeneratedMessageV3.alwaysUseFieldBuilders ? - getImuDataFieldBuilder() : null; - } else { - imuDataBuilder_.addAllMessages(other.imuData_); - } - } - } - if (pdrDataBuilder_ == null) { - if (!other.pdrData_.isEmpty()) { - if (pdrData_.isEmpty()) { - pdrData_ = other.pdrData_; - bitField0_ = (bitField0_ & ~0x00000004); - } else { - ensurePdrDataIsMutable(); - pdrData_.addAll(other.pdrData_); - } - onChanged(); - } - } else { - if (!other.pdrData_.isEmpty()) { - if (pdrDataBuilder_.isEmpty()) { - pdrDataBuilder_.dispose(); - pdrDataBuilder_ = null; - pdrData_ = other.pdrData_; - bitField0_ = (bitField0_ & ~0x00000004); - pdrDataBuilder_ = - com.google.protobuf.GeneratedMessageV3.alwaysUseFieldBuilders ? - getPdrDataFieldBuilder() : null; - } else { - pdrDataBuilder_.addAllMessages(other.pdrData_); - } - } - } - if (positionDataBuilder_ == null) { - if (!other.positionData_.isEmpty()) { - if (positionData_.isEmpty()) { - positionData_ = other.positionData_; - bitField0_ = (bitField0_ & ~0x00000008); - } else { - ensurePositionDataIsMutable(); - positionData_.addAll(other.positionData_); - } - onChanged(); - } - } else { - if (!other.positionData_.isEmpty()) { - if (positionDataBuilder_.isEmpty()) { - positionDataBuilder_.dispose(); - positionDataBuilder_ = null; - positionData_ = other.positionData_; - bitField0_ = (bitField0_ & ~0x00000008); - positionDataBuilder_ = - com.google.protobuf.GeneratedMessageV3.alwaysUseFieldBuilders ? - getPositionDataFieldBuilder() : null; - } else { - positionDataBuilder_.addAllMessages(other.positionData_); - } - } - } - if (pressureDataBuilder_ == null) { - if (!other.pressureData_.isEmpty()) { - if (pressureData_.isEmpty()) { - pressureData_ = other.pressureData_; - bitField0_ = (bitField0_ & ~0x00000010); - } else { - ensurePressureDataIsMutable(); - pressureData_.addAll(other.pressureData_); - } - onChanged(); - } - } else { - if (!other.pressureData_.isEmpty()) { - if (pressureDataBuilder_.isEmpty()) { - pressureDataBuilder_.dispose(); - pressureDataBuilder_ = null; - pressureData_ = other.pressureData_; - bitField0_ = (bitField0_ & ~0x00000010); - pressureDataBuilder_ = - com.google.protobuf.GeneratedMessageV3.alwaysUseFieldBuilders ? - getPressureDataFieldBuilder() : null; - } else { - pressureDataBuilder_.addAllMessages(other.pressureData_); - } - } - } - if (lightDataBuilder_ == null) { - if (!other.lightData_.isEmpty()) { - if (lightData_.isEmpty()) { - lightData_ = other.lightData_; - bitField0_ = (bitField0_ & ~0x00000020); - } else { - ensureLightDataIsMutable(); - lightData_.addAll(other.lightData_); - } - onChanged(); - } - } else { - if (!other.lightData_.isEmpty()) { - if (lightDataBuilder_.isEmpty()) { - lightDataBuilder_.dispose(); - lightDataBuilder_ = null; - lightData_ = other.lightData_; - bitField0_ = (bitField0_ & ~0x00000020); - lightDataBuilder_ = - com.google.protobuf.GeneratedMessageV3.alwaysUseFieldBuilders ? - getLightDataFieldBuilder() : null; - } else { - lightDataBuilder_.addAllMessages(other.lightData_); - } - } - } - if (gnssDataBuilder_ == null) { - if (!other.gnssData_.isEmpty()) { - if (gnssData_.isEmpty()) { - gnssData_ = other.gnssData_; - bitField0_ = (bitField0_ & ~0x00000040); - } else { - ensureGnssDataIsMutable(); - gnssData_.addAll(other.gnssData_); - } - onChanged(); - } - } else { - if (!other.gnssData_.isEmpty()) { - if (gnssDataBuilder_.isEmpty()) { - gnssDataBuilder_.dispose(); - gnssDataBuilder_ = null; - gnssData_ = other.gnssData_; - bitField0_ = (bitField0_ & ~0x00000040); - gnssDataBuilder_ = - com.google.protobuf.GeneratedMessageV3.alwaysUseFieldBuilders ? - getGnssDataFieldBuilder() : null; - } else { - gnssDataBuilder_.addAllMessages(other.gnssData_); - } - } - } - if (wifiDataBuilder_ == null) { - if (!other.wifiData_.isEmpty()) { - if (wifiData_.isEmpty()) { - wifiData_ = other.wifiData_; - bitField0_ = (bitField0_ & ~0x00000080); - } else { - ensureWifiDataIsMutable(); - wifiData_.addAll(other.wifiData_); - } - onChanged(); - } - } else { - if (!other.wifiData_.isEmpty()) { - if (wifiDataBuilder_.isEmpty()) { - wifiDataBuilder_.dispose(); - wifiDataBuilder_ = null; - wifiData_ = other.wifiData_; - bitField0_ = (bitField0_ & ~0x00000080); - wifiDataBuilder_ = - com.google.protobuf.GeneratedMessageV3.alwaysUseFieldBuilders ? - getWifiDataFieldBuilder() : null; - } else { - wifiDataBuilder_.addAllMessages(other.wifiData_); - } - } - } - if (apsDataBuilder_ == null) { - if (!other.apsData_.isEmpty()) { - if (apsData_.isEmpty()) { - apsData_ = other.apsData_; - bitField0_ = (bitField0_ & ~0x00000100); - } else { - ensureApsDataIsMutable(); - apsData_.addAll(other.apsData_); - } - onChanged(); - } - } else { - if (!other.apsData_.isEmpty()) { - if (apsDataBuilder_.isEmpty()) { - apsDataBuilder_.dispose(); - apsDataBuilder_ = null; - apsData_ = other.apsData_; - bitField0_ = (bitField0_ & ~0x00000100); - apsDataBuilder_ = - com.google.protobuf.GeneratedMessageV3.alwaysUseFieldBuilders ? - getApsDataFieldBuilder() : null; - } else { - apsDataBuilder_.addAllMessages(other.apsData_); - } - } - } - if (other.getStartTimestamp() != 0L) { - setStartTimestamp(other.getStartTimestamp()); - } - if (!other.getDataIdentifier().isEmpty()) { - dataIdentifier_ = other.dataIdentifier_; - onChanged(); - } - if (other.hasAccelerometerInfo()) { - mergeAccelerometerInfo(other.getAccelerometerInfo()); - } - if (other.hasGyroscopeInfo()) { - mergeGyroscopeInfo(other.getGyroscopeInfo()); - } - if (other.hasRotationVectorInfo()) { - mergeRotationVectorInfo(other.getRotationVectorInfo()); - } - if (other.hasMagnetometerInfo()) { - mergeMagnetometerInfo(other.getMagnetometerInfo()); - } - if (other.hasBarometerInfo()) { - mergeBarometerInfo(other.getBarometerInfo()); - } - if (other.hasLightSensorInfo()) { - mergeLightSensorInfo(other.getLightSensorInfo()); - } - onChanged(); - return this; - } - - public final boolean isInitialized() { - return true; - } - - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - Trajectory parsedMessage = null; - try { - parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - parsedMessage = (Trajectory) e.getUnfinishedMessage(); - throw e.unwrapIOException(); - } finally { - if (parsedMessage != null) { - mergeFrom(parsedMessage); - } - } - return this; - } - private int bitField0_; - - private Object androidVersion_ = ""; - /** - * optional string android_version = 1; - */ - public String getAndroidVersion() { - Object ref = androidVersion_; - if (!(ref instanceof String)) { - com.google.protobuf.ByteString bs = - (com.google.protobuf.ByteString) ref; - String s = bs.toStringUtf8(); - androidVersion_ = s; - return s; - } else { - return (String) ref; - } - } - /** - * optional string android_version = 1; - */ - public com.google.protobuf.ByteString - getAndroidVersionBytes() { - Object ref = androidVersion_; - if (ref instanceof String) { - com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8( - (String) ref); - androidVersion_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - /** - * optional string android_version = 1; - */ - public Builder setAndroidVersion( - String value) { - if (value == null) { - throw new NullPointerException(); - } - - androidVersion_ = value; - onChanged(); - return this; - } - /** - * optional string android_version = 1; - */ - public Builder clearAndroidVersion() { - - androidVersion_ = getDefaultInstance().getAndroidVersion(); - onChanged(); - return this; - } - /** - * optional string android_version = 1; - */ - public Builder setAndroidVersionBytes( - com.google.protobuf.ByteString value) { - if (value == null) { - throw new NullPointerException(); - } - checkByteStringIsUtf8(value); - - androidVersion_ = value; - onChanged(); - return this; - } - - private java.util.List imuData_ = - java.util.Collections.emptyList(); - private void ensureImuDataIsMutable() { - if (!((bitField0_ & 0x00000002) == 0x00000002)) { - imuData_ = new java.util.ArrayList(imuData_); - bitField0_ |= 0x00000002; - } - } - - private com.google.protobuf.RepeatedFieldBuilderV3< - Motion_Sample, Motion_Sample.Builder, Motion_SampleOrBuilder> imuDataBuilder_; - - /** - * repeated .Motion_Sample imu_data = 2; - */ - public java.util.List getImuDataList() { - if (imuDataBuilder_ == null) { - return java.util.Collections.unmodifiableList(imuData_); - } else { - return imuDataBuilder_.getMessageList(); - } - } - /** - * repeated .Motion_Sample imu_data = 2; - */ - public int getImuDataCount() { - if (imuDataBuilder_ == null) { - return imuData_.size(); - } else { - return imuDataBuilder_.getCount(); - } - } - /** - * repeated .Motion_Sample imu_data = 2; - */ - public Motion_Sample getImuData(int index) { - if (imuDataBuilder_ == null) { - return imuData_.get(index); - } else { - return imuDataBuilder_.getMessage(index); - } - } - /** - * repeated .Motion_Sample imu_data = 2; - */ - public Builder setImuData( - int index, Motion_Sample value) { - if (imuDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensureImuDataIsMutable(); - imuData_.set(index, value); - onChanged(); - } else { - imuDataBuilder_.setMessage(index, value); - } - return this; - } - /** - * repeated .Motion_Sample imu_data = 2; - */ - public Builder setImuData( - int index, Motion_Sample.Builder builderForValue) { - if (imuDataBuilder_ == null) { - ensureImuDataIsMutable(); - imuData_.set(index, builderForValue.build()); - onChanged(); - } else { - imuDataBuilder_.setMessage(index, builderForValue.build()); - } - return this; - } - /** - * repeated .Motion_Sample imu_data = 2; - */ - public Builder addImuData(Motion_Sample value) { - if (imuDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensureImuDataIsMutable(); - imuData_.add(value); - onChanged(); - } else { - imuDataBuilder_.addMessage(value); - } - return this; - } - /** - * repeated .Motion_Sample imu_data = 2; - */ - public Builder addImuData( - int index, Motion_Sample value) { - if (imuDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensureImuDataIsMutable(); - imuData_.add(index, value); - onChanged(); - } else { - imuDataBuilder_.addMessage(index, value); - } - return this; - } - /** - * repeated .Motion_Sample imu_data = 2; - */ - public Builder addImuData( - Motion_Sample.Builder builderForValue) { - if (imuDataBuilder_ == null) { - ensureImuDataIsMutable(); - imuData_.add(builderForValue.build()); - onChanged(); - } else { - imuDataBuilder_.addMessage(builderForValue.build()); - } - return this; - } - /** - * repeated .Motion_Sample imu_data = 2; - */ - public Builder addImuData( - int index, Motion_Sample.Builder builderForValue) { - if (imuDataBuilder_ == null) { - ensureImuDataIsMutable(); - imuData_.add(index, builderForValue.build()); - onChanged(); - } else { - imuDataBuilder_.addMessage(index, builderForValue.build()); - } - return this; - } - /** - * repeated .Motion_Sample imu_data = 2; - */ - public Builder addAllImuData( - Iterable values) { - if (imuDataBuilder_ == null) { - ensureImuDataIsMutable(); - com.google.protobuf.AbstractMessageLite.Builder.addAll( - values, imuData_); - onChanged(); - } else { - imuDataBuilder_.addAllMessages(values); - } - return this; - } - /** - * repeated .Motion_Sample imu_data = 2; - */ - public Builder clearImuData() { - if (imuDataBuilder_ == null) { - imuData_ = java.util.Collections.emptyList(); - bitField0_ = (bitField0_ & ~0x00000002); - onChanged(); - } else { - imuDataBuilder_.clear(); - } - return this; - } - /** - * repeated .Motion_Sample imu_data = 2; - */ - public Builder removeImuData(int index) { - if (imuDataBuilder_ == null) { - ensureImuDataIsMutable(); - imuData_.remove(index); - onChanged(); - } else { - imuDataBuilder_.remove(index); - } - return this; - } - /** - * repeated .Motion_Sample imu_data = 2; - */ - public Motion_Sample.Builder getImuDataBuilder( - int index) { - return getImuDataFieldBuilder().getBuilder(index); - } - /** - * repeated .Motion_Sample imu_data = 2; - */ - public Motion_SampleOrBuilder getImuDataOrBuilder( - int index) { - if (imuDataBuilder_ == null) { - return imuData_.get(index); } else { - return imuDataBuilder_.getMessageOrBuilder(index); - } - } - /** - * repeated .Motion_Sample imu_data = 2; - */ - public java.util.List - getImuDataOrBuilderList() { - if (imuDataBuilder_ != null) { - return imuDataBuilder_.getMessageOrBuilderList(); - } else { - return java.util.Collections.unmodifiableList(imuData_); - } - } - /** - * repeated .Motion_Sample imu_data = 2; - */ - public Motion_Sample.Builder addImuDataBuilder() { - return getImuDataFieldBuilder().addBuilder( - Motion_Sample.getDefaultInstance()); - } - /** - * repeated .Motion_Sample imu_data = 2; - */ - public Motion_Sample.Builder addImuDataBuilder( - int index) { - return getImuDataFieldBuilder().addBuilder( - index, Motion_Sample.getDefaultInstance()); - } - /** - * repeated .Motion_Sample imu_data = 2; - */ - public java.util.List - getImuDataBuilderList() { - return getImuDataFieldBuilder().getBuilderList(); - } - private com.google.protobuf.RepeatedFieldBuilderV3< - Motion_Sample, Motion_Sample.Builder, Motion_SampleOrBuilder> - getImuDataFieldBuilder() { - if (imuDataBuilder_ == null) { - imuDataBuilder_ = new com.google.protobuf.RepeatedFieldBuilderV3< - Motion_Sample, Motion_Sample.Builder, Motion_SampleOrBuilder>( - imuData_, - ((bitField0_ & 0x00000002) == 0x00000002), - getParentForChildren(), - isClean()); - imuData_ = null; - } - return imuDataBuilder_; - } - - private java.util.List pdrData_ = - java.util.Collections.emptyList(); - private void ensurePdrDataIsMutable() { - if (!((bitField0_ & 0x00000004) == 0x00000004)) { - pdrData_ = new java.util.ArrayList(pdrData_); - bitField0_ |= 0x00000004; - } - } - - private com.google.protobuf.RepeatedFieldBuilderV3< - Pdr_Sample, Pdr_Sample.Builder, Pdr_SampleOrBuilder> pdrDataBuilder_; - - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public java.util.List getPdrDataList() { - if (pdrDataBuilder_ == null) { - return java.util.Collections.unmodifiableList(pdrData_); - } else { - return pdrDataBuilder_.getMessageList(); - } - } - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public int getPdrDataCount() { - if (pdrDataBuilder_ == null) { - return pdrData_.size(); - } else { - return pdrDataBuilder_.getCount(); - } - } - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public Pdr_Sample getPdrData(int index) { - if (pdrDataBuilder_ == null) { - return pdrData_.get(index); - } else { - return pdrDataBuilder_.getMessage(index); - } - } - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public Builder setPdrData( - int index, Pdr_Sample value) { - if (pdrDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensurePdrDataIsMutable(); - pdrData_.set(index, value); - onChanged(); - } else { - pdrDataBuilder_.setMessage(index, value); - } - return this; - } - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public Builder setPdrData( - int index, Pdr_Sample.Builder builderForValue) { - if (pdrDataBuilder_ == null) { - ensurePdrDataIsMutable(); - pdrData_.set(index, builderForValue.build()); - onChanged(); - } else { - pdrDataBuilder_.setMessage(index, builderForValue.build()); - } - return this; - } - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public Builder addPdrData(Pdr_Sample value) { - if (pdrDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensurePdrDataIsMutable(); - pdrData_.add(value); - onChanged(); - } else { - pdrDataBuilder_.addMessage(value); - } - return this; - } - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public Builder addPdrData( - int index, Pdr_Sample value) { - if (pdrDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensurePdrDataIsMutable(); - pdrData_.add(index, value); - onChanged(); - } else { - pdrDataBuilder_.addMessage(index, value); - } - return this; - } - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public Builder addPdrData( - Pdr_Sample.Builder builderForValue) { - if (pdrDataBuilder_ == null) { - ensurePdrDataIsMutable(); - pdrData_.add(builderForValue.build()); - onChanged(); - } else { - pdrDataBuilder_.addMessage(builderForValue.build()); - } - return this; - } - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public Builder addPdrData( - int index, Pdr_Sample.Builder builderForValue) { - if (pdrDataBuilder_ == null) { - ensurePdrDataIsMutable(); - pdrData_.add(index, builderForValue.build()); - onChanged(); - } else { - pdrDataBuilder_.addMessage(index, builderForValue.build()); - } - return this; - } - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public Builder addAllPdrData( - Iterable values) { - if (pdrDataBuilder_ == null) { - ensurePdrDataIsMutable(); - com.google.protobuf.AbstractMessageLite.Builder.addAll( - values, pdrData_); - onChanged(); - } else { - pdrDataBuilder_.addAllMessages(values); - } - return this; - } - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public Builder clearPdrData() { - if (pdrDataBuilder_ == null) { - pdrData_ = java.util.Collections.emptyList(); - bitField0_ = (bitField0_ & ~0x00000004); - onChanged(); - } else { - pdrDataBuilder_.clear(); - } - return this; - } - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public Builder removePdrData(int index) { - if (pdrDataBuilder_ == null) { - ensurePdrDataIsMutable(); - pdrData_.remove(index); - onChanged(); - } else { - pdrDataBuilder_.remove(index); - } - return this; - } - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public Pdr_Sample.Builder getPdrDataBuilder( - int index) { - return getPdrDataFieldBuilder().getBuilder(index); - } - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public Pdr_SampleOrBuilder getPdrDataOrBuilder( - int index) { - if (pdrDataBuilder_ == null) { - return pdrData_.get(index); } else { - return pdrDataBuilder_.getMessageOrBuilder(index); - } - } - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public java.util.List - getPdrDataOrBuilderList() { - if (pdrDataBuilder_ != null) { - return pdrDataBuilder_.getMessageOrBuilderList(); - } else { - return java.util.Collections.unmodifiableList(pdrData_); - } - } - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public Pdr_Sample.Builder addPdrDataBuilder() { - return getPdrDataFieldBuilder().addBuilder( - Pdr_Sample.getDefaultInstance()); - } - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public Pdr_Sample.Builder addPdrDataBuilder( - int index) { - return getPdrDataFieldBuilder().addBuilder( - index, Pdr_Sample.getDefaultInstance()); - } - /** - * repeated .Pdr_Sample pdr_data = 3; - */ - public java.util.List - getPdrDataBuilderList() { - return getPdrDataFieldBuilder().getBuilderList(); - } - private com.google.protobuf.RepeatedFieldBuilderV3< - Pdr_Sample, Pdr_Sample.Builder, Pdr_SampleOrBuilder> - getPdrDataFieldBuilder() { - if (pdrDataBuilder_ == null) { - pdrDataBuilder_ = new com.google.protobuf.RepeatedFieldBuilderV3< - Pdr_Sample, Pdr_Sample.Builder, Pdr_SampleOrBuilder>( - pdrData_, - ((bitField0_ & 0x00000004) == 0x00000004), - getParentForChildren(), - isClean()); - pdrData_ = null; - } - return pdrDataBuilder_; - } - - private java.util.List positionData_ = - java.util.Collections.emptyList(); - private void ensurePositionDataIsMutable() { - if (!((bitField0_ & 0x00000008) == 0x00000008)) { - positionData_ = new java.util.ArrayList(positionData_); - bitField0_ |= 0x00000008; - } - } - - private com.google.protobuf.RepeatedFieldBuilderV3< - Position_Sample, Position_Sample.Builder, Position_SampleOrBuilder> positionDataBuilder_; - - /** - * repeated .Position_Sample position_data = 4; - */ - public java.util.List getPositionDataList() { - if (positionDataBuilder_ == null) { - return java.util.Collections.unmodifiableList(positionData_); - } else { - return positionDataBuilder_.getMessageList(); - } - } - /** - * repeated .Position_Sample position_data = 4; - */ - public int getPositionDataCount() { - if (positionDataBuilder_ == null) { - return positionData_.size(); - } else { - return positionDataBuilder_.getCount(); - } - } - /** - * repeated .Position_Sample position_data = 4; - */ - public Position_Sample getPositionData(int index) { - if (positionDataBuilder_ == null) { - return positionData_.get(index); - } else { - return positionDataBuilder_.getMessage(index); - } - } - /** - * repeated .Position_Sample position_data = 4; - */ - public Builder setPositionData( - int index, Position_Sample value) { - if (positionDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensurePositionDataIsMutable(); - positionData_.set(index, value); - onChanged(); - } else { - positionDataBuilder_.setMessage(index, value); - } - return this; - } - /** - * repeated .Position_Sample position_data = 4; - */ - public Builder setPositionData( - int index, Position_Sample.Builder builderForValue) { - if (positionDataBuilder_ == null) { - ensurePositionDataIsMutable(); - positionData_.set(index, builderForValue.build()); - onChanged(); - } else { - positionDataBuilder_.setMessage(index, builderForValue.build()); - } - return this; - } - /** - * repeated .Position_Sample position_data = 4; - */ - public Builder addPositionData(Position_Sample value) { - if (positionDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensurePositionDataIsMutable(); - positionData_.add(value); - onChanged(); - } else { - positionDataBuilder_.addMessage(value); - } - return this; - } - /** - * repeated .Position_Sample position_data = 4; - */ - public Builder addPositionData( - int index, Position_Sample value) { - if (positionDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensurePositionDataIsMutable(); - positionData_.add(index, value); - onChanged(); - } else { - positionDataBuilder_.addMessage(index, value); - } - return this; - } - /** - * repeated .Position_Sample position_data = 4; - */ - public Builder addPositionData( - Position_Sample.Builder builderForValue) { - if (positionDataBuilder_ == null) { - ensurePositionDataIsMutable(); - positionData_.add(builderForValue.build()); - onChanged(); - } else { - positionDataBuilder_.addMessage(builderForValue.build()); - } - return this; - } - /** - * repeated .Position_Sample position_data = 4; - */ - public Builder addPositionData( - int index, Position_Sample.Builder builderForValue) { - if (positionDataBuilder_ == null) { - ensurePositionDataIsMutable(); - positionData_.add(index, builderForValue.build()); - onChanged(); - } else { - positionDataBuilder_.addMessage(index, builderForValue.build()); - } - return this; - } - /** - * repeated .Position_Sample position_data = 4; - */ - public Builder addAllPositionData( - Iterable values) { - if (positionDataBuilder_ == null) { - ensurePositionDataIsMutable(); - com.google.protobuf.AbstractMessageLite.Builder.addAll( - values, positionData_); - onChanged(); - } else { - positionDataBuilder_.addAllMessages(values); - } - return this; - } - /** - * repeated .Position_Sample position_data = 4; - */ - public Builder clearPositionData() { - if (positionDataBuilder_ == null) { - positionData_ = java.util.Collections.emptyList(); - bitField0_ = (bitField0_ & ~0x00000008); - onChanged(); - } else { - positionDataBuilder_.clear(); - } - return this; - } - /** - * repeated .Position_Sample position_data = 4; - */ - public Builder removePositionData(int index) { - if (positionDataBuilder_ == null) { - ensurePositionDataIsMutable(); - positionData_.remove(index); - onChanged(); - } else { - positionDataBuilder_.remove(index); - } - return this; - } - /** - * repeated .Position_Sample position_data = 4; - */ - public Position_Sample.Builder getPositionDataBuilder( - int index) { - return getPositionDataFieldBuilder().getBuilder(index); - } - /** - * repeated .Position_Sample position_data = 4; - */ - public Position_SampleOrBuilder getPositionDataOrBuilder( - int index) { - if (positionDataBuilder_ == null) { - return positionData_.get(index); } else { - return positionDataBuilder_.getMessageOrBuilder(index); - } - } - /** - * repeated .Position_Sample position_data = 4; - */ - public java.util.List - getPositionDataOrBuilderList() { - if (positionDataBuilder_ != null) { - return positionDataBuilder_.getMessageOrBuilderList(); - } else { - return java.util.Collections.unmodifiableList(positionData_); - } - } - /** - * repeated .Position_Sample position_data = 4; - */ - public Position_Sample.Builder addPositionDataBuilder() { - return getPositionDataFieldBuilder().addBuilder( - Position_Sample.getDefaultInstance()); - } - /** - * repeated .Position_Sample position_data = 4; - */ - public Position_Sample.Builder addPositionDataBuilder( - int index) { - return getPositionDataFieldBuilder().addBuilder( - index, Position_Sample.getDefaultInstance()); - } - /** - * repeated .Position_Sample position_data = 4; - */ - public java.util.List - getPositionDataBuilderList() { - return getPositionDataFieldBuilder().getBuilderList(); - } - private com.google.protobuf.RepeatedFieldBuilderV3< - Position_Sample, Position_Sample.Builder, Position_SampleOrBuilder> - getPositionDataFieldBuilder() { - if (positionDataBuilder_ == null) { - positionDataBuilder_ = new com.google.protobuf.RepeatedFieldBuilderV3< - Position_Sample, Position_Sample.Builder, Position_SampleOrBuilder>( - positionData_, - ((bitField0_ & 0x00000008) == 0x00000008), - getParentForChildren(), - isClean()); - positionData_ = null; - } - return positionDataBuilder_; - } - - private java.util.List pressureData_ = - java.util.Collections.emptyList(); - private void ensurePressureDataIsMutable() { - if (!((bitField0_ & 0x00000010) == 0x00000010)) { - pressureData_ = new java.util.ArrayList(pressureData_); - bitField0_ |= 0x00000010; - } - } - - private com.google.protobuf.RepeatedFieldBuilderV3< - Pressure_Sample, Pressure_Sample.Builder, Pressure_SampleOrBuilder> pressureDataBuilder_; - - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public java.util.List getPressureDataList() { - if (pressureDataBuilder_ == null) { - return java.util.Collections.unmodifiableList(pressureData_); - } else { - return pressureDataBuilder_.getMessageList(); - } - } - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public int getPressureDataCount() { - if (pressureDataBuilder_ == null) { - return pressureData_.size(); - } else { - return pressureDataBuilder_.getCount(); - } - } - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public Pressure_Sample getPressureData(int index) { - if (pressureDataBuilder_ == null) { - return pressureData_.get(index); - } else { - return pressureDataBuilder_.getMessage(index); - } - } - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public Builder setPressureData( - int index, Pressure_Sample value) { - if (pressureDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensurePressureDataIsMutable(); - pressureData_.set(index, value); - onChanged(); - } else { - pressureDataBuilder_.setMessage(index, value); - } - return this; - } - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public Builder setPressureData( - int index, Pressure_Sample.Builder builderForValue) { - if (pressureDataBuilder_ == null) { - ensurePressureDataIsMutable(); - pressureData_.set(index, builderForValue.build()); - onChanged(); - } else { - pressureDataBuilder_.setMessage(index, builderForValue.build()); - } - return this; - } - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public Builder addPressureData(Pressure_Sample value) { - if (pressureDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensurePressureDataIsMutable(); - pressureData_.add(value); - onChanged(); - } else { - pressureDataBuilder_.addMessage(value); - } - return this; - } - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public Builder addPressureData( - int index, Pressure_Sample value) { - if (pressureDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensurePressureDataIsMutable(); - pressureData_.add(index, value); - onChanged(); - } else { - pressureDataBuilder_.addMessage(index, value); - } - return this; - } - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public Builder addPressureData( - Pressure_Sample.Builder builderForValue) { - if (pressureDataBuilder_ == null) { - ensurePressureDataIsMutable(); - pressureData_.add(builderForValue.build()); - onChanged(); - } else { - pressureDataBuilder_.addMessage(builderForValue.build()); - } - return this; - } - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public Builder addPressureData( - int index, Pressure_Sample.Builder builderForValue) { - if (pressureDataBuilder_ == null) { - ensurePressureDataIsMutable(); - pressureData_.add(index, builderForValue.build()); - onChanged(); - } else { - pressureDataBuilder_.addMessage(index, builderForValue.build()); - } - return this; - } - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public Builder addAllPressureData( - Iterable values) { - if (pressureDataBuilder_ == null) { - ensurePressureDataIsMutable(); - com.google.protobuf.AbstractMessageLite.Builder.addAll( - values, pressureData_); - onChanged(); - } else { - pressureDataBuilder_.addAllMessages(values); - } - return this; - } - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public Builder clearPressureData() { - if (pressureDataBuilder_ == null) { - pressureData_ = java.util.Collections.emptyList(); - bitField0_ = (bitField0_ & ~0x00000010); - onChanged(); - } else { - pressureDataBuilder_.clear(); - } - return this; - } - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public Builder removePressureData(int index) { - if (pressureDataBuilder_ == null) { - ensurePressureDataIsMutable(); - pressureData_.remove(index); - onChanged(); - } else { - pressureDataBuilder_.remove(index); - } - return this; - } - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public Pressure_Sample.Builder getPressureDataBuilder( - int index) { - return getPressureDataFieldBuilder().getBuilder(index); - } - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public Pressure_SampleOrBuilder getPressureDataOrBuilder( - int index) { - if (pressureDataBuilder_ == null) { - return pressureData_.get(index); } else { - return pressureDataBuilder_.getMessageOrBuilder(index); - } - } - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public java.util.List - getPressureDataOrBuilderList() { - if (pressureDataBuilder_ != null) { - return pressureDataBuilder_.getMessageOrBuilderList(); - } else { - return java.util.Collections.unmodifiableList(pressureData_); - } - } - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public Pressure_Sample.Builder addPressureDataBuilder() { - return getPressureDataFieldBuilder().addBuilder( - Pressure_Sample.getDefaultInstance()); - } - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public Pressure_Sample.Builder addPressureDataBuilder( - int index) { - return getPressureDataFieldBuilder().addBuilder( - index, Pressure_Sample.getDefaultInstance()); - } - /** - * repeated .Pressure_Sample pressure_data = 5; - */ - public java.util.List - getPressureDataBuilderList() { - return getPressureDataFieldBuilder().getBuilderList(); - } - private com.google.protobuf.RepeatedFieldBuilderV3< - Pressure_Sample, Pressure_Sample.Builder, Pressure_SampleOrBuilder> - getPressureDataFieldBuilder() { - if (pressureDataBuilder_ == null) { - pressureDataBuilder_ = new com.google.protobuf.RepeatedFieldBuilderV3< - Pressure_Sample, Pressure_Sample.Builder, Pressure_SampleOrBuilder>( - pressureData_, - ((bitField0_ & 0x00000010) == 0x00000010), - getParentForChildren(), - isClean()); - pressureData_ = null; - } - return pressureDataBuilder_; - } - - private java.util.List lightData_ = - java.util.Collections.emptyList(); - private void ensureLightDataIsMutable() { - if (!((bitField0_ & 0x00000020) == 0x00000020)) { - lightData_ = new java.util.ArrayList(lightData_); - bitField0_ |= 0x00000020; - } - } - - private com.google.protobuf.RepeatedFieldBuilderV3< - Light_Sample, Light_Sample.Builder, Light_SampleOrBuilder> lightDataBuilder_; - - /** - * repeated .Light_Sample light_data = 6; - */ - public java.util.List getLightDataList() { - if (lightDataBuilder_ == null) { - return java.util.Collections.unmodifiableList(lightData_); - } else { - return lightDataBuilder_.getMessageList(); - } - } - /** - * repeated .Light_Sample light_data = 6; - */ - public int getLightDataCount() { - if (lightDataBuilder_ == null) { - return lightData_.size(); - } else { - return lightDataBuilder_.getCount(); - } - } - /** - * repeated .Light_Sample light_data = 6; - */ - public Light_Sample getLightData(int index) { - if (lightDataBuilder_ == null) { - return lightData_.get(index); - } else { - return lightDataBuilder_.getMessage(index); - } - } - /** - * repeated .Light_Sample light_data = 6; - */ - public Builder setLightData( - int index, Light_Sample value) { - if (lightDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensureLightDataIsMutable(); - lightData_.set(index, value); - onChanged(); - } else { - lightDataBuilder_.setMessage(index, value); - } - return this; - } - /** - * repeated .Light_Sample light_data = 6; - */ - public Builder setLightData( - int index, Light_Sample.Builder builderForValue) { - if (lightDataBuilder_ == null) { - ensureLightDataIsMutable(); - lightData_.set(index, builderForValue.build()); - onChanged(); - } else { - lightDataBuilder_.setMessage(index, builderForValue.build()); - } - return this; - } - /** - * repeated .Light_Sample light_data = 6; - */ - public Builder addLightData(Light_Sample value) { - if (lightDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensureLightDataIsMutable(); - lightData_.add(value); - onChanged(); - } else { - lightDataBuilder_.addMessage(value); - } - return this; - } - /** - * repeated .Light_Sample light_data = 6; - */ - public Builder addLightData( - int index, Light_Sample value) { - if (lightDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensureLightDataIsMutable(); - lightData_.add(index, value); - onChanged(); - } else { - lightDataBuilder_.addMessage(index, value); - } - return this; - } - /** - * repeated .Light_Sample light_data = 6; - */ - public Builder addLightData( - Light_Sample.Builder builderForValue) { - if (lightDataBuilder_ == null) { - ensureLightDataIsMutable(); - lightData_.add(builderForValue.build()); - onChanged(); - } else { - lightDataBuilder_.addMessage(builderForValue.build()); - } - return this; - } - /** - * repeated .Light_Sample light_data = 6; - */ - public Builder addLightData( - int index, Light_Sample.Builder builderForValue) { - if (lightDataBuilder_ == null) { - ensureLightDataIsMutable(); - lightData_.add(index, builderForValue.build()); - onChanged(); - } else { - lightDataBuilder_.addMessage(index, builderForValue.build()); - } - return this; - } - /** - * repeated .Light_Sample light_data = 6; - */ - public Builder addAllLightData( - Iterable values) { - if (lightDataBuilder_ == null) { - ensureLightDataIsMutable(); - com.google.protobuf.AbstractMessageLite.Builder.addAll( - values, lightData_); - onChanged(); - } else { - lightDataBuilder_.addAllMessages(values); - } - return this; - } - /** - * repeated .Light_Sample light_data = 6; - */ - public Builder clearLightData() { - if (lightDataBuilder_ == null) { - lightData_ = java.util.Collections.emptyList(); - bitField0_ = (bitField0_ & ~0x00000020); - onChanged(); - } else { - lightDataBuilder_.clear(); - } - return this; - } - /** - * repeated .Light_Sample light_data = 6; - */ - public Builder removeLightData(int index) { - if (lightDataBuilder_ == null) { - ensureLightDataIsMutable(); - lightData_.remove(index); - onChanged(); - } else { - lightDataBuilder_.remove(index); - } - return this; - } - /** - * repeated .Light_Sample light_data = 6; - */ - public Light_Sample.Builder getLightDataBuilder( - int index) { - return getLightDataFieldBuilder().getBuilder(index); - } - /** - * repeated .Light_Sample light_data = 6; - */ - public Light_SampleOrBuilder getLightDataOrBuilder( - int index) { - if (lightDataBuilder_ == null) { - return lightData_.get(index); } else { - return lightDataBuilder_.getMessageOrBuilder(index); - } - } - /** - * repeated .Light_Sample light_data = 6; - */ - public java.util.List - getLightDataOrBuilderList() { - if (lightDataBuilder_ != null) { - return lightDataBuilder_.getMessageOrBuilderList(); - } else { - return java.util.Collections.unmodifiableList(lightData_); - } - } - /** - * repeated .Light_Sample light_data = 6; - */ - public Light_Sample.Builder addLightDataBuilder() { - return getLightDataFieldBuilder().addBuilder( - Light_Sample.getDefaultInstance()); - } - /** - * repeated .Light_Sample light_data = 6; - */ - public Light_Sample.Builder addLightDataBuilder( - int index) { - return getLightDataFieldBuilder().addBuilder( - index, Light_Sample.getDefaultInstance()); - } - /** - * repeated .Light_Sample light_data = 6; - */ - public java.util.List - getLightDataBuilderList() { - return getLightDataFieldBuilder().getBuilderList(); - } - private com.google.protobuf.RepeatedFieldBuilderV3< - Light_Sample, Light_Sample.Builder, Light_SampleOrBuilder> - getLightDataFieldBuilder() { - if (lightDataBuilder_ == null) { - lightDataBuilder_ = new com.google.protobuf.RepeatedFieldBuilderV3< - Light_Sample, Light_Sample.Builder, Light_SampleOrBuilder>( - lightData_, - ((bitField0_ & 0x00000020) == 0x00000020), - getParentForChildren(), - isClean()); - lightData_ = null; - } - return lightDataBuilder_; - } - - private java.util.List gnssData_ = - java.util.Collections.emptyList(); - private void ensureGnssDataIsMutable() { - if (!((bitField0_ & 0x00000040) == 0x00000040)) { - gnssData_ = new java.util.ArrayList(gnssData_); - bitField0_ |= 0x00000040; - } - } - - private com.google.protobuf.RepeatedFieldBuilderV3< - GNSS_Sample, GNSS_Sample.Builder, GNSS_SampleOrBuilder> gnssDataBuilder_; - - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public java.util.List getGnssDataList() { - if (gnssDataBuilder_ == null) { - return java.util.Collections.unmodifiableList(gnssData_); - } else { - return gnssDataBuilder_.getMessageList(); - } - } - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public int getGnssDataCount() { - if (gnssDataBuilder_ == null) { - return gnssData_.size(); - } else { - return gnssDataBuilder_.getCount(); - } - } - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public GNSS_Sample getGnssData(int index) { - if (gnssDataBuilder_ == null) { - return gnssData_.get(index); - } else { - return gnssDataBuilder_.getMessage(index); - } - } - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public Builder setGnssData( - int index, GNSS_Sample value) { - if (gnssDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensureGnssDataIsMutable(); - gnssData_.set(index, value); - onChanged(); - } else { - gnssDataBuilder_.setMessage(index, value); - } - return this; - } - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public Builder setGnssData( - int index, GNSS_Sample.Builder builderForValue) { - if (gnssDataBuilder_ == null) { - ensureGnssDataIsMutable(); - gnssData_.set(index, builderForValue.build()); - onChanged(); - } else { - gnssDataBuilder_.setMessage(index, builderForValue.build()); - } - return this; - } - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public Builder addGnssData(GNSS_Sample value) { - if (gnssDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensureGnssDataIsMutable(); - gnssData_.add(value); - onChanged(); - } else { - gnssDataBuilder_.addMessage(value); - } - return this; - } - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public Builder addGnssData( - int index, GNSS_Sample value) { - if (gnssDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensureGnssDataIsMutable(); - gnssData_.add(index, value); - onChanged(); - } else { - gnssDataBuilder_.addMessage(index, value); - } - return this; - } - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public Builder addGnssData( - GNSS_Sample.Builder builderForValue) { - if (gnssDataBuilder_ == null) { - ensureGnssDataIsMutable(); - gnssData_.add(builderForValue.build()); - onChanged(); - } else { - gnssDataBuilder_.addMessage(builderForValue.build()); - } - return this; - } - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public Builder addGnssData( - int index, GNSS_Sample.Builder builderForValue) { - if (gnssDataBuilder_ == null) { - ensureGnssDataIsMutable(); - gnssData_.add(index, builderForValue.build()); - onChanged(); - } else { - gnssDataBuilder_.addMessage(index, builderForValue.build()); - } - return this; - } - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public Builder addAllGnssData( - Iterable values) { - if (gnssDataBuilder_ == null) { - ensureGnssDataIsMutable(); - com.google.protobuf.AbstractMessageLite.Builder.addAll( - values, gnssData_); - onChanged(); - } else { - gnssDataBuilder_.addAllMessages(values); - } - return this; - } - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public Builder clearGnssData() { - if (gnssDataBuilder_ == null) { - gnssData_ = java.util.Collections.emptyList(); - bitField0_ = (bitField0_ & ~0x00000040); - onChanged(); - } else { - gnssDataBuilder_.clear(); - } - return this; - } - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public Builder removeGnssData(int index) { - if (gnssDataBuilder_ == null) { - ensureGnssDataIsMutable(); - gnssData_.remove(index); - onChanged(); - } else { - gnssDataBuilder_.remove(index); - } - return this; - } - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public GNSS_Sample.Builder getGnssDataBuilder( - int index) { - return getGnssDataFieldBuilder().getBuilder(index); - } - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public GNSS_SampleOrBuilder getGnssDataOrBuilder( - int index) { - if (gnssDataBuilder_ == null) { - return gnssData_.get(index); } else { - return gnssDataBuilder_.getMessageOrBuilder(index); - } - } - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public java.util.List - getGnssDataOrBuilderList() { - if (gnssDataBuilder_ != null) { - return gnssDataBuilder_.getMessageOrBuilderList(); - } else { - return java.util.Collections.unmodifiableList(gnssData_); - } - } - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public GNSS_Sample.Builder addGnssDataBuilder() { - return getGnssDataFieldBuilder().addBuilder( - GNSS_Sample.getDefaultInstance()); - } - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public GNSS_Sample.Builder addGnssDataBuilder( - int index) { - return getGnssDataFieldBuilder().addBuilder( - index, GNSS_Sample.getDefaultInstance()); - } - /** - * repeated .GNSS_Sample gnss_data = 7; - */ - public java.util.List - getGnssDataBuilderList() { - return getGnssDataFieldBuilder().getBuilderList(); - } - private com.google.protobuf.RepeatedFieldBuilderV3< - GNSS_Sample, GNSS_Sample.Builder, GNSS_SampleOrBuilder> - getGnssDataFieldBuilder() { - if (gnssDataBuilder_ == null) { - gnssDataBuilder_ = new com.google.protobuf.RepeatedFieldBuilderV3< - GNSS_Sample, GNSS_Sample.Builder, GNSS_SampleOrBuilder>( - gnssData_, - ((bitField0_ & 0x00000040) == 0x00000040), - getParentForChildren(), - isClean()); - gnssData_ = null; - } - return gnssDataBuilder_; - } - - private java.util.List wifiData_ = - java.util.Collections.emptyList(); - private void ensureWifiDataIsMutable() { - if (!((bitField0_ & 0x00000080) == 0x00000080)) { - wifiData_ = new java.util.ArrayList(wifiData_); - bitField0_ |= 0x00000080; - } - } - - private com.google.protobuf.RepeatedFieldBuilderV3< - WiFi_Sample, WiFi_Sample.Builder, WiFi_SampleOrBuilder> wifiDataBuilder_; - - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public java.util.List getWifiDataList() { - if (wifiDataBuilder_ == null) { - return java.util.Collections.unmodifiableList(wifiData_); - } else { - return wifiDataBuilder_.getMessageList(); - } - } - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public int getWifiDataCount() { - if (wifiDataBuilder_ == null) { - return wifiData_.size(); - } else { - return wifiDataBuilder_.getCount(); - } - } - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public WiFi_Sample getWifiData(int index) { - if (wifiDataBuilder_ == null) { - return wifiData_.get(index); - } else { - return wifiDataBuilder_.getMessage(index); - } - } - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public Builder setWifiData( - int index, WiFi_Sample value) { - if (wifiDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensureWifiDataIsMutable(); - wifiData_.set(index, value); - onChanged(); - } else { - wifiDataBuilder_.setMessage(index, value); - } - return this; - } - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public Builder setWifiData( - int index, WiFi_Sample.Builder builderForValue) { - if (wifiDataBuilder_ == null) { - ensureWifiDataIsMutable(); - wifiData_.set(index, builderForValue.build()); - onChanged(); - } else { - wifiDataBuilder_.setMessage(index, builderForValue.build()); - } - return this; - } - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public Builder addWifiData(WiFi_Sample value) { - if (wifiDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensureWifiDataIsMutable(); - wifiData_.add(value); - onChanged(); - } else { - wifiDataBuilder_.addMessage(value); - } - return this; - } - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public Builder addWifiData( - int index, WiFi_Sample value) { - if (wifiDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensureWifiDataIsMutable(); - wifiData_.add(index, value); - onChanged(); - } else { - wifiDataBuilder_.addMessage(index, value); - } - return this; - } - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public Builder addWifiData( - WiFi_Sample.Builder builderForValue) { - if (wifiDataBuilder_ == null) { - ensureWifiDataIsMutable(); - wifiData_.add(builderForValue.build()); - onChanged(); - } else { - wifiDataBuilder_.addMessage(builderForValue.build()); - } - return this; - } - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public Builder addWifiData( - int index, WiFi_Sample.Builder builderForValue) { - if (wifiDataBuilder_ == null) { - ensureWifiDataIsMutable(); - wifiData_.add(index, builderForValue.build()); - onChanged(); - } else { - wifiDataBuilder_.addMessage(index, builderForValue.build()); - } - return this; - } - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public Builder addAllWifiData( - Iterable values) { - if (wifiDataBuilder_ == null) { - ensureWifiDataIsMutable(); - com.google.protobuf.AbstractMessageLite.Builder.addAll( - values, wifiData_); - onChanged(); - } else { - wifiDataBuilder_.addAllMessages(values); - } - return this; - } - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public Builder clearWifiData() { - if (wifiDataBuilder_ == null) { - wifiData_ = java.util.Collections.emptyList(); - bitField0_ = (bitField0_ & ~0x00000080); - onChanged(); - } else { - wifiDataBuilder_.clear(); - } - return this; - } - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public Builder removeWifiData(int index) { - if (wifiDataBuilder_ == null) { - ensureWifiDataIsMutable(); - wifiData_.remove(index); - onChanged(); - } else { - wifiDataBuilder_.remove(index); - } - return this; - } - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public WiFi_Sample.Builder getWifiDataBuilder( - int index) { - return getWifiDataFieldBuilder().getBuilder(index); - } - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public WiFi_SampleOrBuilder getWifiDataOrBuilder( - int index) { - if (wifiDataBuilder_ == null) { - return wifiData_.get(index); } else { - return wifiDataBuilder_.getMessageOrBuilder(index); - } - } - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public java.util.List - getWifiDataOrBuilderList() { - if (wifiDataBuilder_ != null) { - return wifiDataBuilder_.getMessageOrBuilderList(); - } else { - return java.util.Collections.unmodifiableList(wifiData_); - } - } - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public WiFi_Sample.Builder addWifiDataBuilder() { - return getWifiDataFieldBuilder().addBuilder( - WiFi_Sample.getDefaultInstance()); - } - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public WiFi_Sample.Builder addWifiDataBuilder( - int index) { - return getWifiDataFieldBuilder().addBuilder( - index, WiFi_Sample.getDefaultInstance()); - } - /** - * repeated .WiFi_Sample wifi_data = 8; - */ - public java.util.List - getWifiDataBuilderList() { - return getWifiDataFieldBuilder().getBuilderList(); - } - private com.google.protobuf.RepeatedFieldBuilderV3< - WiFi_Sample, WiFi_Sample.Builder, WiFi_SampleOrBuilder> - getWifiDataFieldBuilder() { - if (wifiDataBuilder_ == null) { - wifiDataBuilder_ = new com.google.protobuf.RepeatedFieldBuilderV3< - WiFi_Sample, WiFi_Sample.Builder, WiFi_SampleOrBuilder>( - wifiData_, - ((bitField0_ & 0x00000080) == 0x00000080), - getParentForChildren(), - isClean()); - wifiData_ = null; - } - return wifiDataBuilder_; - } - - private java.util.List apsData_ = - java.util.Collections.emptyList(); - private void ensureApsDataIsMutable() { - if (!((bitField0_ & 0x00000100) == 0x00000100)) { - apsData_ = new java.util.ArrayList(apsData_); - bitField0_ |= 0x00000100; - } - } - - private com.google.protobuf.RepeatedFieldBuilderV3< - AP_Data, AP_Data.Builder, AP_DataOrBuilder> apsDataBuilder_; - - /** - * repeated .AP_Data aps_data = 9; - */ - public java.util.List getApsDataList() { - if (apsDataBuilder_ == null) { - return java.util.Collections.unmodifiableList(apsData_); - } else { - return apsDataBuilder_.getMessageList(); - } - } - /** - * repeated .AP_Data aps_data = 9; - */ - public int getApsDataCount() { - if (apsDataBuilder_ == null) { - return apsData_.size(); - } else { - return apsDataBuilder_.getCount(); - } - } - /** - * repeated .AP_Data aps_data = 9; - */ - public AP_Data getApsData(int index) { - if (apsDataBuilder_ == null) { - return apsData_.get(index); - } else { - return apsDataBuilder_.getMessage(index); - } - } - /** - * repeated .AP_Data aps_data = 9; - */ - public Builder setApsData( - int index, AP_Data value) { - if (apsDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensureApsDataIsMutable(); - apsData_.set(index, value); - onChanged(); - } else { - apsDataBuilder_.setMessage(index, value); - } - return this; - } - /** - * repeated .AP_Data aps_data = 9; - */ - public Builder setApsData( - int index, AP_Data.Builder builderForValue) { - if (apsDataBuilder_ == null) { - ensureApsDataIsMutable(); - apsData_.set(index, builderForValue.build()); - onChanged(); - } else { - apsDataBuilder_.setMessage(index, builderForValue.build()); - } - return this; - } - /** - * repeated .AP_Data aps_data = 9; - */ - public Builder addApsData(AP_Data value) { - if (apsDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensureApsDataIsMutable(); - apsData_.add(value); - onChanged(); - } else { - apsDataBuilder_.addMessage(value); - } - return this; - } - /** - * repeated .AP_Data aps_data = 9; - */ - public Builder addApsData( - int index, AP_Data value) { - if (apsDataBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensureApsDataIsMutable(); - apsData_.add(index, value); - onChanged(); - } else { - apsDataBuilder_.addMessage(index, value); - } - return this; - } - /** - * repeated .AP_Data aps_data = 9; - */ - public Builder addApsData( - AP_Data.Builder builderForValue) { - if (apsDataBuilder_ == null) { - ensureApsDataIsMutable(); - apsData_.add(builderForValue.build()); - onChanged(); - } else { - apsDataBuilder_.addMessage(builderForValue.build()); - } - return this; - } - /** - * repeated .AP_Data aps_data = 9; - */ - public Builder addApsData( - int index, AP_Data.Builder builderForValue) { - if (apsDataBuilder_ == null) { - ensureApsDataIsMutable(); - apsData_.add(index, builderForValue.build()); - onChanged(); - } else { - apsDataBuilder_.addMessage(index, builderForValue.build()); - } - return this; - } - /** - * repeated .AP_Data aps_data = 9; - */ - public Builder addAllApsData( - Iterable values) { - if (apsDataBuilder_ == null) { - ensureApsDataIsMutable(); - com.google.protobuf.AbstractMessageLite.Builder.addAll( - values, apsData_); - onChanged(); - } else { - apsDataBuilder_.addAllMessages(values); - } - return this; - } - /** - * repeated .AP_Data aps_data = 9; - */ - public Builder clearApsData() { - if (apsDataBuilder_ == null) { - apsData_ = java.util.Collections.emptyList(); - bitField0_ = (bitField0_ & ~0x00000100); - onChanged(); - } else { - apsDataBuilder_.clear(); - } - return this; - } - /** - * repeated .AP_Data aps_data = 9; - */ - public Builder removeApsData(int index) { - if (apsDataBuilder_ == null) { - ensureApsDataIsMutable(); - apsData_.remove(index); - onChanged(); - } else { - apsDataBuilder_.remove(index); - } - return this; - } - /** - * repeated .AP_Data aps_data = 9; - */ - public AP_Data.Builder getApsDataBuilder( - int index) { - return getApsDataFieldBuilder().getBuilder(index); - } - /** - * repeated .AP_Data aps_data = 9; - */ - public AP_DataOrBuilder getApsDataOrBuilder( - int index) { - if (apsDataBuilder_ == null) { - return apsData_.get(index); } else { - return apsDataBuilder_.getMessageOrBuilder(index); - } - } - /** - * repeated .AP_Data aps_data = 9; - */ - public java.util.List - getApsDataOrBuilderList() { - if (apsDataBuilder_ != null) { - return apsDataBuilder_.getMessageOrBuilderList(); - } else { - return java.util.Collections.unmodifiableList(apsData_); - } - } - /** - * repeated .AP_Data aps_data = 9; - */ - public AP_Data.Builder addApsDataBuilder() { - return getApsDataFieldBuilder().addBuilder( - AP_Data.getDefaultInstance()); - } - /** - * repeated .AP_Data aps_data = 9; - */ - public AP_Data.Builder addApsDataBuilder( - int index) { - return getApsDataFieldBuilder().addBuilder( - index, AP_Data.getDefaultInstance()); - } - /** - * repeated .AP_Data aps_data = 9; - */ - public java.util.List - getApsDataBuilderList() { - return getApsDataFieldBuilder().getBuilderList(); - } - private com.google.protobuf.RepeatedFieldBuilderV3< - AP_Data, AP_Data.Builder, AP_DataOrBuilder> - getApsDataFieldBuilder() { - if (apsDataBuilder_ == null) { - apsDataBuilder_ = new com.google.protobuf.RepeatedFieldBuilderV3< - AP_Data, AP_Data.Builder, AP_DataOrBuilder>( - apsData_, - ((bitField0_ & 0x00000100) == 0x00000100), - getParentForChildren(), - isClean()); - apsData_ = null; - } - return apsDataBuilder_; - } - - private long startTimestamp_ ; - /** - *
-       * UNIX timestamp (in milliseconds) recorded from the start of this
-       * trajectory data collection event. All future
-       * timestamps in sub classes are to be RELATIVE timestamps
-       * (in milliseconds) to this start time.
-       * E.g.
-       * start_timestamp = 1674819807315 (UTC 27 Jan 2023 in the morning)
-       * relative_timestamp = 3000 (3s)
-       * 
- * - * optional int64 start_timestamp = 10; - */ - public long getStartTimestamp() { - return startTimestamp_; - } - /** - *
-       * UNIX timestamp (in milliseconds) recorded from the start of this
-       * trajectory data collection event. All future
-       * timestamps in sub classes are to be RELATIVE timestamps
-       * (in milliseconds) to this start time.
-       * E.g.
-       * start_timestamp = 1674819807315 (UTC 27 Jan 2023 in the morning)
-       * relative_timestamp = 3000 (3s)
-       * 
- * - * optional int64 start_timestamp = 10; - */ - public Builder setStartTimestamp(long value) { - - startTimestamp_ = value; - onChanged(); - return this; - } - /** - *
-       * UNIX timestamp (in milliseconds) recorded from the start of this
-       * trajectory data collection event. All future
-       * timestamps in sub classes are to be RELATIVE timestamps
-       * (in milliseconds) to this start time.
-       * E.g.
-       * start_timestamp = 1674819807315 (UTC 27 Jan 2023 in the morning)
-       * relative_timestamp = 3000 (3s)
-       * 
- * - * optional int64 start_timestamp = 10; - */ - public Builder clearStartTimestamp() { - - startTimestamp_ = 0L; - onChanged(); - return this; - } - - private Object dataIdentifier_ = ""; - /** - * optional string data_identifier = 11; - */ - public String getDataIdentifier() { - Object ref = dataIdentifier_; - if (!(ref instanceof String)) { - com.google.protobuf.ByteString bs = - (com.google.protobuf.ByteString) ref; - String s = bs.toStringUtf8(); - dataIdentifier_ = s; - return s; - } else { - return (String) ref; - } - } - /** - * optional string data_identifier = 11; - */ - public com.google.protobuf.ByteString - getDataIdentifierBytes() { - Object ref = dataIdentifier_; - if (ref instanceof String) { - com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8( - (String) ref); - dataIdentifier_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - /** - * optional string data_identifier = 11; - */ - public Builder setDataIdentifier( - String value) { - if (value == null) { - throw new NullPointerException(); - } - - dataIdentifier_ = value; - onChanged(); - return this; - } - /** - * optional string data_identifier = 11; - */ - public Builder clearDataIdentifier() { - - dataIdentifier_ = getDefaultInstance().getDataIdentifier(); - onChanged(); - return this; - } - /** - * optional string data_identifier = 11; - */ - public Builder setDataIdentifierBytes( - com.google.protobuf.ByteString value) { - if (value == null) { - throw new NullPointerException(); - } - checkByteStringIsUtf8(value); - - dataIdentifier_ = value; - onChanged(); - return this; - } - - private Sensor_Info accelerometerInfo_ = null; - private com.google.protobuf.SingleFieldBuilderV3< - Sensor_Info, Sensor_Info.Builder, Sensor_InfoOrBuilder> accelerometerInfoBuilder_; - /** - * optional .Sensor_Info accelerometer_info = 12; - */ - public boolean hasAccelerometerInfo() { - return accelerometerInfoBuilder_ != null || accelerometerInfo_ != null; - } - /** - * optional .Sensor_Info accelerometer_info = 12; - */ - public Sensor_Info getAccelerometerInfo() { - if (accelerometerInfoBuilder_ == null) { - return accelerometerInfo_ == null ? Sensor_Info.getDefaultInstance() : accelerometerInfo_; - } else { - return accelerometerInfoBuilder_.getMessage(); - } - } - /** - * optional .Sensor_Info accelerometer_info = 12; - */ - public Builder setAccelerometerInfo(Sensor_Info value) { - if (accelerometerInfoBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - accelerometerInfo_ = value; - onChanged(); - } else { - accelerometerInfoBuilder_.setMessage(value); - } - - return this; - } - /** - * optional .Sensor_Info accelerometer_info = 12; - */ - public Builder setAccelerometerInfo( - Sensor_Info.Builder builderForValue) { - if (accelerometerInfoBuilder_ == null) { - accelerometerInfo_ = builderForValue.build(); - onChanged(); - } else { - accelerometerInfoBuilder_.setMessage(builderForValue.build()); - } - - return this; - } - /** - * optional .Sensor_Info accelerometer_info = 12; - */ - public Builder mergeAccelerometerInfo(Sensor_Info value) { - if (accelerometerInfoBuilder_ == null) { - if (accelerometerInfo_ != null) { - accelerometerInfo_ = - Sensor_Info.newBuilder(accelerometerInfo_).mergeFrom(value).buildPartial(); - } else { - accelerometerInfo_ = value; - } - onChanged(); - } else { - accelerometerInfoBuilder_.mergeFrom(value); - } - - return this; - } - /** - * optional .Sensor_Info accelerometer_info = 12; - */ - public Builder clearAccelerometerInfo() { - if (accelerometerInfoBuilder_ == null) { - accelerometerInfo_ = null; - onChanged(); - } else { - accelerometerInfo_ = null; - accelerometerInfoBuilder_ = null; - } - - return this; - } - /** - * optional .Sensor_Info accelerometer_info = 12; - */ - public Sensor_Info.Builder getAccelerometerInfoBuilder() { - - onChanged(); - return getAccelerometerInfoFieldBuilder().getBuilder(); - } - /** - * optional .Sensor_Info accelerometer_info = 12; - */ - public Sensor_InfoOrBuilder getAccelerometerInfoOrBuilder() { - if (accelerometerInfoBuilder_ != null) { - return accelerometerInfoBuilder_.getMessageOrBuilder(); - } else { - return accelerometerInfo_ == null ? - Sensor_Info.getDefaultInstance() : accelerometerInfo_; - } - } - /** - * optional .Sensor_Info accelerometer_info = 12; - */ - private com.google.protobuf.SingleFieldBuilderV3< - Sensor_Info, Sensor_Info.Builder, Sensor_InfoOrBuilder> - getAccelerometerInfoFieldBuilder() { - if (accelerometerInfoBuilder_ == null) { - accelerometerInfoBuilder_ = new com.google.protobuf.SingleFieldBuilderV3< - Sensor_Info, Sensor_Info.Builder, Sensor_InfoOrBuilder>( - getAccelerometerInfo(), - getParentForChildren(), - isClean()); - accelerometerInfo_ = null; - } - return accelerometerInfoBuilder_; - } - - private Sensor_Info gyroscopeInfo_ = null; - private com.google.protobuf.SingleFieldBuilderV3< - Sensor_Info, Sensor_Info.Builder, Sensor_InfoOrBuilder> gyroscopeInfoBuilder_; - /** - * optional .Sensor_Info gyroscope_info = 13; - */ - public boolean hasGyroscopeInfo() { - return gyroscopeInfoBuilder_ != null || gyroscopeInfo_ != null; - } - /** - * optional .Sensor_Info gyroscope_info = 13; - */ - public Sensor_Info getGyroscopeInfo() { - if (gyroscopeInfoBuilder_ == null) { - return gyroscopeInfo_ == null ? Sensor_Info.getDefaultInstance() : gyroscopeInfo_; - } else { - return gyroscopeInfoBuilder_.getMessage(); - } - } - /** - * optional .Sensor_Info gyroscope_info = 13; - */ - public Builder setGyroscopeInfo(Sensor_Info value) { - if (gyroscopeInfoBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - gyroscopeInfo_ = value; - onChanged(); - } else { - gyroscopeInfoBuilder_.setMessage(value); - } - - return this; - } - /** - * optional .Sensor_Info gyroscope_info = 13; - */ - public Builder setGyroscopeInfo( - Sensor_Info.Builder builderForValue) { - if (gyroscopeInfoBuilder_ == null) { - gyroscopeInfo_ = builderForValue.build(); - onChanged(); - } else { - gyroscopeInfoBuilder_.setMessage(builderForValue.build()); - } - - return this; - } - /** - * optional .Sensor_Info gyroscope_info = 13; - */ - public Builder mergeGyroscopeInfo(Sensor_Info value) { - if (gyroscopeInfoBuilder_ == null) { - if (gyroscopeInfo_ != null) { - gyroscopeInfo_ = - Sensor_Info.newBuilder(gyroscopeInfo_).mergeFrom(value).buildPartial(); - } else { - gyroscopeInfo_ = value; - } - onChanged(); - } else { - gyroscopeInfoBuilder_.mergeFrom(value); - } - - return this; - } - /** - * optional .Sensor_Info gyroscope_info = 13; - */ - public Builder clearGyroscopeInfo() { - if (gyroscopeInfoBuilder_ == null) { - gyroscopeInfo_ = null; - onChanged(); - } else { - gyroscopeInfo_ = null; - gyroscopeInfoBuilder_ = null; - } - - return this; - } - /** - * optional .Sensor_Info gyroscope_info = 13; - */ - public Sensor_Info.Builder getGyroscopeInfoBuilder() { - - onChanged(); - return getGyroscopeInfoFieldBuilder().getBuilder(); - } - /** - * optional .Sensor_Info gyroscope_info = 13; - */ - public Sensor_InfoOrBuilder getGyroscopeInfoOrBuilder() { - if (gyroscopeInfoBuilder_ != null) { - return gyroscopeInfoBuilder_.getMessageOrBuilder(); - } else { - return gyroscopeInfo_ == null ? - Sensor_Info.getDefaultInstance() : gyroscopeInfo_; - } - } - /** - * optional .Sensor_Info gyroscope_info = 13; - */ - private com.google.protobuf.SingleFieldBuilderV3< - Sensor_Info, Sensor_Info.Builder, Sensor_InfoOrBuilder> - getGyroscopeInfoFieldBuilder() { - if (gyroscopeInfoBuilder_ == null) { - gyroscopeInfoBuilder_ = new com.google.protobuf.SingleFieldBuilderV3< - Sensor_Info, Sensor_Info.Builder, Sensor_InfoOrBuilder>( - getGyroscopeInfo(), - getParentForChildren(), - isClean()); - gyroscopeInfo_ = null; - } - return gyroscopeInfoBuilder_; - } - - private Sensor_Info rotationVectorInfo_ = null; - private com.google.protobuf.SingleFieldBuilderV3< - Sensor_Info, Sensor_Info.Builder, Sensor_InfoOrBuilder> rotationVectorInfoBuilder_; - /** - * optional .Sensor_Info rotation_vector_info = 14; - */ - public boolean hasRotationVectorInfo() { - return rotationVectorInfoBuilder_ != null || rotationVectorInfo_ != null; - } - /** - * optional .Sensor_Info rotation_vector_info = 14; - */ - public Sensor_Info getRotationVectorInfo() { - if (rotationVectorInfoBuilder_ == null) { - return rotationVectorInfo_ == null ? Sensor_Info.getDefaultInstance() : rotationVectorInfo_; - } else { - return rotationVectorInfoBuilder_.getMessage(); - } - } - /** - * optional .Sensor_Info rotation_vector_info = 14; - */ - public Builder setRotationVectorInfo(Sensor_Info value) { - if (rotationVectorInfoBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - rotationVectorInfo_ = value; - onChanged(); - } else { - rotationVectorInfoBuilder_.setMessage(value); - } - - return this; - } - /** - * optional .Sensor_Info rotation_vector_info = 14; - */ - public Builder setRotationVectorInfo( - Sensor_Info.Builder builderForValue) { - if (rotationVectorInfoBuilder_ == null) { - rotationVectorInfo_ = builderForValue.build(); - onChanged(); - } else { - rotationVectorInfoBuilder_.setMessage(builderForValue.build()); - } - - return this; - } - /** - * optional .Sensor_Info rotation_vector_info = 14; - */ - public Builder mergeRotationVectorInfo(Sensor_Info value) { - if (rotationVectorInfoBuilder_ == null) { - if (rotationVectorInfo_ != null) { - rotationVectorInfo_ = - Sensor_Info.newBuilder(rotationVectorInfo_).mergeFrom(value).buildPartial(); - } else { - rotationVectorInfo_ = value; - } - onChanged(); - } else { - rotationVectorInfoBuilder_.mergeFrom(value); - } - - return this; - } - /** - * optional .Sensor_Info rotation_vector_info = 14; - */ - public Builder clearRotationVectorInfo() { - if (rotationVectorInfoBuilder_ == null) { - rotationVectorInfo_ = null; - onChanged(); - } else { - rotationVectorInfo_ = null; - rotationVectorInfoBuilder_ = null; - } - - return this; - } - /** - * optional .Sensor_Info rotation_vector_info = 14; - */ - public Sensor_Info.Builder getRotationVectorInfoBuilder() { - - onChanged(); - return getRotationVectorInfoFieldBuilder().getBuilder(); - } - /** - * optional .Sensor_Info rotation_vector_info = 14; - */ - public Sensor_InfoOrBuilder getRotationVectorInfoOrBuilder() { - if (rotationVectorInfoBuilder_ != null) { - return rotationVectorInfoBuilder_.getMessageOrBuilder(); - } else { - return rotationVectorInfo_ == null ? - Sensor_Info.getDefaultInstance() : rotationVectorInfo_; - } - } - /** - * optional .Sensor_Info rotation_vector_info = 14; - */ - private com.google.protobuf.SingleFieldBuilderV3< - Sensor_Info, Sensor_Info.Builder, Sensor_InfoOrBuilder> - getRotationVectorInfoFieldBuilder() { - if (rotationVectorInfoBuilder_ == null) { - rotationVectorInfoBuilder_ = new com.google.protobuf.SingleFieldBuilderV3< - Sensor_Info, Sensor_Info.Builder, Sensor_InfoOrBuilder>( - getRotationVectorInfo(), - getParentForChildren(), - isClean()); - rotationVectorInfo_ = null; - } - return rotationVectorInfoBuilder_; - } - - private Sensor_Info magnetometerInfo_ = null; - private com.google.protobuf.SingleFieldBuilderV3< - Sensor_Info, Sensor_Info.Builder, Sensor_InfoOrBuilder> magnetometerInfoBuilder_; - /** - * optional .Sensor_Info magnetometer_info = 15; - */ - public boolean hasMagnetometerInfo() { - return magnetometerInfoBuilder_ != null || magnetometerInfo_ != null; - } - /** - * optional .Sensor_Info magnetometer_info = 15; - */ - public Sensor_Info getMagnetometerInfo() { - if (magnetometerInfoBuilder_ == null) { - return magnetometerInfo_ == null ? Sensor_Info.getDefaultInstance() : magnetometerInfo_; - } else { - return magnetometerInfoBuilder_.getMessage(); - } - } - /** - * optional .Sensor_Info magnetometer_info = 15; - */ - public Builder setMagnetometerInfo(Sensor_Info value) { - if (magnetometerInfoBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - magnetometerInfo_ = value; - onChanged(); - } else { - magnetometerInfoBuilder_.setMessage(value); - } - - return this; - } - /** - * optional .Sensor_Info magnetometer_info = 15; - */ - public Builder setMagnetometerInfo( - Sensor_Info.Builder builderForValue) { - if (magnetometerInfoBuilder_ == null) { - magnetometerInfo_ = builderForValue.build(); - onChanged(); - } else { - magnetometerInfoBuilder_.setMessage(builderForValue.build()); - } - - return this; - } - /** - * optional .Sensor_Info magnetometer_info = 15; - */ - public Builder mergeMagnetometerInfo(Sensor_Info value) { - if (magnetometerInfoBuilder_ == null) { - if (magnetometerInfo_ != null) { - magnetometerInfo_ = - Sensor_Info.newBuilder(magnetometerInfo_).mergeFrom(value).buildPartial(); - } else { - magnetometerInfo_ = value; - } - onChanged(); - } else { - magnetometerInfoBuilder_.mergeFrom(value); - } - - return this; - } - /** - * optional .Sensor_Info magnetometer_info = 15; - */ - public Builder clearMagnetometerInfo() { - if (magnetometerInfoBuilder_ == null) { - magnetometerInfo_ = null; - onChanged(); - } else { - magnetometerInfo_ = null; - magnetometerInfoBuilder_ = null; - } - - return this; - } - /** - * optional .Sensor_Info magnetometer_info = 15; - */ - public Sensor_Info.Builder getMagnetometerInfoBuilder() { - - onChanged(); - return getMagnetometerInfoFieldBuilder().getBuilder(); - } - /** - * optional .Sensor_Info magnetometer_info = 15; - */ - public Sensor_InfoOrBuilder getMagnetometerInfoOrBuilder() { - if (magnetometerInfoBuilder_ != null) { - return magnetometerInfoBuilder_.getMessageOrBuilder(); - } else { - return magnetometerInfo_ == null ? - Sensor_Info.getDefaultInstance() : magnetometerInfo_; - } - } - /** - * optional .Sensor_Info magnetometer_info = 15; - */ - private com.google.protobuf.SingleFieldBuilderV3< - Sensor_Info, Sensor_Info.Builder, Sensor_InfoOrBuilder> - getMagnetometerInfoFieldBuilder() { - if (magnetometerInfoBuilder_ == null) { - magnetometerInfoBuilder_ = new com.google.protobuf.SingleFieldBuilderV3< - Sensor_Info, Sensor_Info.Builder, Sensor_InfoOrBuilder>( - getMagnetometerInfo(), - getParentForChildren(), - isClean()); - magnetometerInfo_ = null; - } - return magnetometerInfoBuilder_; - } - - private Sensor_Info barometerInfo_ = null; - private com.google.protobuf.SingleFieldBuilderV3< - Sensor_Info, Sensor_Info.Builder, Sensor_InfoOrBuilder> barometerInfoBuilder_; - /** - * optional .Sensor_Info barometer_info = 16; - */ - public boolean hasBarometerInfo() { - return barometerInfoBuilder_ != null || barometerInfo_ != null; - } - /** - * optional .Sensor_Info barometer_info = 16; - */ - public Sensor_Info getBarometerInfo() { - if (barometerInfoBuilder_ == null) { - return barometerInfo_ == null ? Sensor_Info.getDefaultInstance() : barometerInfo_; - } else { - return barometerInfoBuilder_.getMessage(); - } - } - /** - * optional .Sensor_Info barometer_info = 16; - */ - public Builder setBarometerInfo(Sensor_Info value) { - if (barometerInfoBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - barometerInfo_ = value; - onChanged(); - } else { - barometerInfoBuilder_.setMessage(value); - } - - return this; - } - /** - * optional .Sensor_Info barometer_info = 16; - */ - public Builder setBarometerInfo( - Sensor_Info.Builder builderForValue) { - if (barometerInfoBuilder_ == null) { - barometerInfo_ = builderForValue.build(); - onChanged(); - } else { - barometerInfoBuilder_.setMessage(builderForValue.build()); - } - - return this; - } - /** - * optional .Sensor_Info barometer_info = 16; - */ - public Builder mergeBarometerInfo(Sensor_Info value) { - if (barometerInfoBuilder_ == null) { - if (barometerInfo_ != null) { - barometerInfo_ = - Sensor_Info.newBuilder(barometerInfo_).mergeFrom(value).buildPartial(); - } else { - barometerInfo_ = value; - } - onChanged(); - } else { - barometerInfoBuilder_.mergeFrom(value); - } - - return this; - } - /** - * optional .Sensor_Info barometer_info = 16; - */ - public Builder clearBarometerInfo() { - if (barometerInfoBuilder_ == null) { - barometerInfo_ = null; - onChanged(); - } else { - barometerInfo_ = null; - barometerInfoBuilder_ = null; - } - - return this; - } - /** - * optional .Sensor_Info barometer_info = 16; - */ - public Sensor_Info.Builder getBarometerInfoBuilder() { - - onChanged(); - return getBarometerInfoFieldBuilder().getBuilder(); - } - /** - * optional .Sensor_Info barometer_info = 16; - */ - public Sensor_InfoOrBuilder getBarometerInfoOrBuilder() { - if (barometerInfoBuilder_ != null) { - return barometerInfoBuilder_.getMessageOrBuilder(); - } else { - return barometerInfo_ == null ? - Sensor_Info.getDefaultInstance() : barometerInfo_; - } - } - /** - * optional .Sensor_Info barometer_info = 16; - */ - private com.google.protobuf.SingleFieldBuilderV3< - Sensor_Info, Sensor_Info.Builder, Sensor_InfoOrBuilder> - getBarometerInfoFieldBuilder() { - if (barometerInfoBuilder_ == null) { - barometerInfoBuilder_ = new com.google.protobuf.SingleFieldBuilderV3< - Sensor_Info, Sensor_Info.Builder, Sensor_InfoOrBuilder>( - getBarometerInfo(), - getParentForChildren(), - isClean()); - barometerInfo_ = null; - } - return barometerInfoBuilder_; - } - - private Sensor_Info lightSensorInfo_ = null; - private com.google.protobuf.SingleFieldBuilderV3< - Sensor_Info, Sensor_Info.Builder, Sensor_InfoOrBuilder> lightSensorInfoBuilder_; - /** - * optional .Sensor_Info light_sensor_info = 17; - */ - public boolean hasLightSensorInfo() { - return lightSensorInfoBuilder_ != null || lightSensorInfo_ != null; - } - /** - * optional .Sensor_Info light_sensor_info = 17; - */ - public Sensor_Info getLightSensorInfo() { - if (lightSensorInfoBuilder_ == null) { - return lightSensorInfo_ == null ? Sensor_Info.getDefaultInstance() : lightSensorInfo_; - } else { - return lightSensorInfoBuilder_.getMessage(); - } - } - /** - * optional .Sensor_Info light_sensor_info = 17; - */ - public Builder setLightSensorInfo(Sensor_Info value) { - if (lightSensorInfoBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - lightSensorInfo_ = value; - onChanged(); - } else { - lightSensorInfoBuilder_.setMessage(value); - } - - return this; - } - /** - * optional .Sensor_Info light_sensor_info = 17; - */ - public Builder setLightSensorInfo( - Sensor_Info.Builder builderForValue) { - if (lightSensorInfoBuilder_ == null) { - lightSensorInfo_ = builderForValue.build(); - onChanged(); - } else { - lightSensorInfoBuilder_.setMessage(builderForValue.build()); - } - - return this; - } - /** - * optional .Sensor_Info light_sensor_info = 17; - */ - public Builder mergeLightSensorInfo(Sensor_Info value) { - if (lightSensorInfoBuilder_ == null) { - if (lightSensorInfo_ != null) { - lightSensorInfo_ = - Sensor_Info.newBuilder(lightSensorInfo_).mergeFrom(value).buildPartial(); - } else { - lightSensorInfo_ = value; - } - onChanged(); - } else { - lightSensorInfoBuilder_.mergeFrom(value); - } - - return this; - } - /** - * optional .Sensor_Info light_sensor_info = 17; - */ - public Builder clearLightSensorInfo() { - if (lightSensorInfoBuilder_ == null) { - lightSensorInfo_ = null; - onChanged(); - } else { - lightSensorInfo_ = null; - lightSensorInfoBuilder_ = null; - } - - return this; - } - /** - * optional .Sensor_Info light_sensor_info = 17; - */ - public Sensor_Info.Builder getLightSensorInfoBuilder() { - - onChanged(); - return getLightSensorInfoFieldBuilder().getBuilder(); - } - /** - * optional .Sensor_Info light_sensor_info = 17; - */ - public Sensor_InfoOrBuilder getLightSensorInfoOrBuilder() { - if (lightSensorInfoBuilder_ != null) { - return lightSensorInfoBuilder_.getMessageOrBuilder(); - } else { - return lightSensorInfo_ == null ? - Sensor_Info.getDefaultInstance() : lightSensorInfo_; - } - } - /** - * optional .Sensor_Info light_sensor_info = 17; - */ - private com.google.protobuf.SingleFieldBuilderV3< - Sensor_Info, Sensor_Info.Builder, Sensor_InfoOrBuilder> - getLightSensorInfoFieldBuilder() { - if (lightSensorInfoBuilder_ == null) { - lightSensorInfoBuilder_ = new com.google.protobuf.SingleFieldBuilderV3< - Sensor_Info, Sensor_Info.Builder, Sensor_InfoOrBuilder>( - getLightSensorInfo(), - getParentForChildren(), - isClean()); - lightSensorInfo_ = null; - } - return lightSensorInfoBuilder_; - } - public final Builder setUnknownFields( - final com.google.protobuf.UnknownFieldSet unknownFields) { - return this; - } - - public final Builder mergeUnknownFields( - final com.google.protobuf.UnknownFieldSet unknownFields) { - return this; - } - - - // @@protoc_insertion_point(builder_scope:Trajectory) - } - - // @@protoc_insertion_point(class_scope:Trajectory) - private static final Trajectory DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new Trajectory(); - } - - public static Trajectory getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser - PARSER = new com.google.protobuf.AbstractParser() { - public Trajectory parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return new Trajectory(input, extensionRegistry); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - public Trajectory getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - - } - - public interface Pdr_SampleOrBuilder extends - // @@protoc_insertion_point(interface_extends:Pdr_Sample) - com.google.protobuf.MessageOrBuilder { - - /** - *
-     * milliseconds from the start_timestamp
-     * 
- * - * optional int64 relative_timestamp = 1; - */ - long getRelativeTimestamp(); - - /** - *
-     * Both in metres. You should implement an algorithm to estimate
-     * these values. The values are always relative to your start point
-     * so the first entry should always be x = 0.0, y = 0.0
-     * 
- * - * optional float x = 2; - */ - float getX(); - - /** - * optional float y = 3; - */ - float getY(); - } - /** - * Protobuf type {@code Pdr_Sample} - */ - public static final class Pdr_Sample extends - com.google.protobuf.GeneratedMessageV3 implements - // @@protoc_insertion_point(message_implements:Pdr_Sample) - Pdr_SampleOrBuilder { - // Use Pdr_Sample.newBuilder() to construct. - private Pdr_Sample(com.google.protobuf.GeneratedMessageV3.Builder builder) { - super(builder); - } - private Pdr_Sample() { - relativeTimestamp_ = 0L; - x_ = 0F; - y_ = 0F; - } - - @Override - public final com.google.protobuf.UnknownFieldSet - getUnknownFields() { - return com.google.protobuf.UnknownFieldSet.getDefaultInstance(); - } - private Pdr_Sample( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - this(); - int mutable_bitField0_ = 0; - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - default: { - if (!input.skipField(tag)) { - done = true; - } - break; - } - case 8: { - - relativeTimestamp_ = input.readInt64(); - break; - } - case 21: { - - x_ = input.readFloat(); - break; - } - case 29: { - - y_ = input.readFloat(); - break; - } - } - } - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(this); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException( - e).setUnfinishedMessage(this); - } finally { - makeExtensionsImmutable(); - } - } - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return Traj.internal_static_Pdr_Sample_descriptor; - } - - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return Traj.internal_static_Pdr_Sample_fieldAccessorTable - .ensureFieldAccessorsInitialized( - Pdr_Sample.class, Builder.class); - } - - public static final int RELATIVE_TIMESTAMP_FIELD_NUMBER = 1; - private long relativeTimestamp_; - /** - *
-     * milliseconds from the start_timestamp
-     * 
- * - * optional int64 relative_timestamp = 1; - */ - public long getRelativeTimestamp() { - return relativeTimestamp_; - } - - public static final int X_FIELD_NUMBER = 2; - private float x_; - /** - *
-     * Both in metres. You should implement an algorithm to estimate
-     * these values. The values are always relative to your start point
-     * so the first entry should always be x = 0.0, y = 0.0
-     * 
- * - * optional float x = 2; - */ - public float getX() { - return x_; - } - - public static final int Y_FIELD_NUMBER = 3; - private float y_; - /** - * optional float y = 3; - */ - public float getY() { - return y_; - } - - private byte memoizedIsInitialized = -1; - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) return true; - if (isInitialized == 0) return false; - - memoizedIsInitialized = 1; - return true; - } - - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - if (relativeTimestamp_ != 0L) { - output.writeInt64(1, relativeTimestamp_); - } - if (x_ != 0F) { - output.writeFloat(2, x_); - } - if (y_ != 0F) { - output.writeFloat(3, y_); - } - } - - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) return size; - - size = 0; - if (relativeTimestamp_ != 0L) { - size += com.google.protobuf.CodedOutputStream - .computeInt64Size(1, relativeTimestamp_); - } - if (x_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(2, x_); - } - if (y_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(3, y_); - } - memoizedSize = size; - return size; - } - - private static final long serialVersionUID = 0L; - @Override - public boolean equals(final Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof Pdr_Sample)) { - return super.equals(obj); - } - Pdr_Sample other = (Pdr_Sample) obj; - - boolean result = true; - result = result && (getRelativeTimestamp() - == other.getRelativeTimestamp()); - result = result && ( - Float.floatToIntBits(getX()) - == Float.floatToIntBits( - other.getX())); - result = result && ( - Float.floatToIntBits(getY()) - == Float.floatToIntBits( - other.getY())); - return result; - } - - @Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptorForType().hashCode(); - hash = (37 * hash) + RELATIVE_TIMESTAMP_FIELD_NUMBER; - hash = (53 * hash) + com.google.protobuf.Internal.hashLong( - getRelativeTimestamp()); - hash = (37 * hash) + X_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getX()); - hash = (37 * hash) + Y_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getY()); - hash = (29 * hash) + unknownFields.hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static Pdr_Sample parseFrom( - com.google.protobuf.ByteString data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static Pdr_Sample parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static Pdr_Sample parseFrom(byte[] data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static Pdr_Sample parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static Pdr_Sample parseFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input); - } - public static Pdr_Sample parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input, extensionRegistry); - } - public static Pdr_Sample parseDelimitedFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseDelimitedWithIOException(PARSER, input); - } - public static Pdr_Sample parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - public static Pdr_Sample parseFrom( - com.google.protobuf.CodedInputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input); - } - public static Pdr_Sample parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public Builder newBuilderForType() { return newBuilder(); } - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - public static Builder newBuilder(Pdr_Sample prototype) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() : new Builder().mergeFrom(this); - } - - @Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { - Builder builder = new Builder(parent); - return builder; - } - /** - * Protobuf type {@code Pdr_Sample} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessageV3.Builder implements - // @@protoc_insertion_point(builder_implements:Pdr_Sample) - Pdr_SampleOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return Traj.internal_static_Pdr_Sample_descriptor; - } - - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return Traj.internal_static_Pdr_Sample_fieldAccessorTable - .ensureFieldAccessorsInitialized( - Pdr_Sample.class, Builder.class); - } - - // Construct using Traj.Pdr_Sample.newBuilder() - private Builder() { - maybeForceBuilderInitialization(); - } - - private Builder( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { - super(parent); - maybeForceBuilderInitialization(); - } - private void maybeForceBuilderInitialization() { - if (com.google.protobuf.GeneratedMessageV3 - .alwaysUseFieldBuilders) { - } - } - public Builder clear() { - super.clear(); - relativeTimestamp_ = 0L; - - x_ = 0F; - - y_ = 0F; - - return this; - } - - public com.google.protobuf.Descriptors.Descriptor - getDescriptorForType() { - return Traj.internal_static_Pdr_Sample_descriptor; - } - - public Pdr_Sample getDefaultInstanceForType() { - return Pdr_Sample.getDefaultInstance(); - } - - public Pdr_Sample build() { - Pdr_Sample result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - public Pdr_Sample buildPartial() { - Pdr_Sample result = new Pdr_Sample(this); - result.relativeTimestamp_ = relativeTimestamp_; - result.x_ = x_; - result.y_ = y_; - onBuilt(); - return result; - } - - public Builder clone() { - return (Builder) super.clone(); - } - public Builder setField( - com.google.protobuf.Descriptors.FieldDescriptor field, - Object value) { - return (Builder) super.setField(field, value); - } - public Builder clearField( - com.google.protobuf.Descriptors.FieldDescriptor field) { - return (Builder) super.clearField(field); - } - public Builder clearOneof( - com.google.protobuf.Descriptors.OneofDescriptor oneof) { - return (Builder) super.clearOneof(oneof); - } - public Builder setRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - int index, Object value) { - return (Builder) super.setRepeatedField(field, index, value); - } - public Builder addRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - Object value) { - return (Builder) super.addRepeatedField(field, value); - } - public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof Pdr_Sample) { - return mergeFrom((Pdr_Sample)other); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom(Pdr_Sample other) { - if (other == Pdr_Sample.getDefaultInstance()) return this; - if (other.getRelativeTimestamp() != 0L) { - setRelativeTimestamp(other.getRelativeTimestamp()); - } - if (other.getX() != 0F) { - setX(other.getX()); - } - if (other.getY() != 0F) { - setY(other.getY()); - } - onChanged(); - return this; - } - - public final boolean isInitialized() { - return true; - } - - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - Pdr_Sample parsedMessage = null; - try { - parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - parsedMessage = (Pdr_Sample) e.getUnfinishedMessage(); - throw e.unwrapIOException(); - } finally { - if (parsedMessage != null) { - mergeFrom(parsedMessage); - } - } - return this; - } - - private long relativeTimestamp_ ; - /** - *
-       * milliseconds from the start_timestamp
-       * 
- * - * optional int64 relative_timestamp = 1; - */ - public long getRelativeTimestamp() { - return relativeTimestamp_; - } - /** - *
-       * milliseconds from the start_timestamp
-       * 
- * - * optional int64 relative_timestamp = 1; - */ - public Builder setRelativeTimestamp(long value) { - - relativeTimestamp_ = value; - onChanged(); - return this; - } - /** - *
-       * milliseconds from the start_timestamp
-       * 
- * - * optional int64 relative_timestamp = 1; - */ - public Builder clearRelativeTimestamp() { - - relativeTimestamp_ = 0L; - onChanged(); - return this; - } - - private float x_ ; - /** - *
-       * Both in metres. You should implement an algorithm to estimate
-       * these values. The values are always relative to your start point
-       * so the first entry should always be x = 0.0, y = 0.0
-       * 
- * - * optional float x = 2; - */ - public float getX() { - return x_; - } - /** - *
-       * Both in metres. You should implement an algorithm to estimate
-       * these values. The values are always relative to your start point
-       * so the first entry should always be x = 0.0, y = 0.0
-       * 
- * - * optional float x = 2; - */ - public Builder setX(float value) { - - x_ = value; - onChanged(); - return this; - } - /** - *
-       * Both in metres. You should implement an algorithm to estimate
-       * these values. The values are always relative to your start point
-       * so the first entry should always be x = 0.0, y = 0.0
-       * 
- * - * optional float x = 2; - */ - public Builder clearX() { - - x_ = 0F; - onChanged(); - return this; - } - - private float y_ ; - /** - * optional float y = 3; - */ - public float getY() { - return y_; - } - /** - * optional float y = 3; - */ - public Builder setY(float value) { - - y_ = value; - onChanged(); - return this; - } - /** - * optional float y = 3; - */ - public Builder clearY() { - - y_ = 0F; - onChanged(); - return this; - } - public final Builder setUnknownFields( - final com.google.protobuf.UnknownFieldSet unknownFields) { - return this; - } - - public final Builder mergeUnknownFields( - final com.google.protobuf.UnknownFieldSet unknownFields) { - return this; - } - - - // @@protoc_insertion_point(builder_scope:Pdr_Sample) - } - - // @@protoc_insertion_point(class_scope:Pdr_Sample) - private static final Pdr_Sample DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new Pdr_Sample(); - } - - public static Pdr_Sample getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser - PARSER = new com.google.protobuf.AbstractParser() { - public Pdr_Sample parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return new Pdr_Sample(input, extensionRegistry); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - public Pdr_Sample getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - - } - - public interface Motion_SampleOrBuilder extends - // @@protoc_insertion_point(interface_extends:Motion_Sample) - com.google.protobuf.MessageOrBuilder { - - /** - *
-     * milliseconds
-     * 
- * - * optional int64 relative_timestamp = 1; - */ - long getRelativeTimestamp(); - - /** - *
-     * m/s^2
-     * 
- * - * optional float acc_x = 2; - */ - float getAccX(); - - /** - * optional float acc_y = 3; - */ - float getAccY(); - - /** - * optional float acc_z = 4; - */ - float getAccZ(); - - /** - *
-     * radians/s
-     * 
- * - * optional float gyr_x = 5; - */ - float getGyrX(); - - /** - * optional float gyr_y = 6; - */ - float getGyrY(); - - /** - * optional float gyr_z = 7; - */ - float getGyrZ(); - - /** - *
-     * unitless, 4 components should sum to ~1
-     * 
- * - * optional float rotation_vector_x = 8; - */ - float getRotationVectorX(); - - /** - * optional float rotation_vector_y = 9; - */ - float getRotationVectorY(); - - /** - * optional float rotation_vector_z = 10; - */ - float getRotationVectorZ(); - - /** - * optional float rotation_vector_w = 11; - */ - float getRotationVectorW(); - - /** - *
-     * Integer
-     * 
- * - * optional int32 step_count = 12; - */ - int getStepCount(); - } - /** - * Protobuf type {@code Motion_Sample} - */ - public static final class Motion_Sample extends - com.google.protobuf.GeneratedMessageV3 implements - // @@protoc_insertion_point(message_implements:Motion_Sample) - Motion_SampleOrBuilder { - // Use Motion_Sample.newBuilder() to construct. - private Motion_Sample(com.google.protobuf.GeneratedMessageV3.Builder builder) { - super(builder); - } - private Motion_Sample() { - relativeTimestamp_ = 0L; - accX_ = 0F; - accY_ = 0F; - accZ_ = 0F; - gyrX_ = 0F; - gyrY_ = 0F; - gyrZ_ = 0F; - rotationVectorX_ = 0F; - rotationVectorY_ = 0F; - rotationVectorZ_ = 0F; - rotationVectorW_ = 0F; - stepCount_ = 0; - } - - @Override - public final com.google.protobuf.UnknownFieldSet - getUnknownFields() { - return com.google.protobuf.UnknownFieldSet.getDefaultInstance(); - } - private Motion_Sample( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - this(); - int mutable_bitField0_ = 0; - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - default: { - if (!input.skipField(tag)) { - done = true; - } - break; - } - case 8: { - - relativeTimestamp_ = input.readInt64(); - break; - } - case 21: { - - accX_ = input.readFloat(); - break; - } - case 29: { - - accY_ = input.readFloat(); - break; - } - case 37: { - - accZ_ = input.readFloat(); - break; - } - case 45: { - - gyrX_ = input.readFloat(); - break; - } - case 53: { - - gyrY_ = input.readFloat(); - break; - } - case 61: { - - gyrZ_ = input.readFloat(); - break; - } - case 69: { - - rotationVectorX_ = input.readFloat(); - break; - } - case 77: { - - rotationVectorY_ = input.readFloat(); - break; - } - case 85: { - - rotationVectorZ_ = input.readFloat(); - break; - } - case 93: { - - rotationVectorW_ = input.readFloat(); - break; - } - case 96: { - - stepCount_ = input.readInt32(); - break; - } - } - } - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(this); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException( - e).setUnfinishedMessage(this); - } finally { - makeExtensionsImmutable(); - } - } - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return Traj.internal_static_Motion_Sample_descriptor; - } - - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return Traj.internal_static_Motion_Sample_fieldAccessorTable - .ensureFieldAccessorsInitialized( - Motion_Sample.class, Builder.class); - } - - public static final int RELATIVE_TIMESTAMP_FIELD_NUMBER = 1; - private long relativeTimestamp_; - /** - *
-     * milliseconds
-     * 
- * - * optional int64 relative_timestamp = 1; - */ - public long getRelativeTimestamp() { - return relativeTimestamp_; - } - - public static final int ACC_X_FIELD_NUMBER = 2; - private float accX_; - /** - *
-     * m/s^2
-     * 
- * - * optional float acc_x = 2; - */ - public float getAccX() { - return accX_; - } - - public static final int ACC_Y_FIELD_NUMBER = 3; - private float accY_; - /** - * optional float acc_y = 3; - */ - public float getAccY() { - return accY_; - } - - public static final int ACC_Z_FIELD_NUMBER = 4; - private float accZ_; - /** - * optional float acc_z = 4; - */ - public float getAccZ() { - return accZ_; - } - - public static final int GYR_X_FIELD_NUMBER = 5; - private float gyrX_; - /** - *
-     * radians/s
-     * 
- * - * optional float gyr_x = 5; - */ - public float getGyrX() { - return gyrX_; - } - - public static final int GYR_Y_FIELD_NUMBER = 6; - private float gyrY_; - /** - * optional float gyr_y = 6; - */ - public float getGyrY() { - return gyrY_; - } - - public static final int GYR_Z_FIELD_NUMBER = 7; - private float gyrZ_; - /** - * optional float gyr_z = 7; - */ - public float getGyrZ() { - return gyrZ_; - } - - public static final int ROTATION_VECTOR_X_FIELD_NUMBER = 8; - private float rotationVectorX_; - /** - *
-     * unitless, 4 components should sum to ~1
-     * 
- * - * optional float rotation_vector_x = 8; - */ - public float getRotationVectorX() { - return rotationVectorX_; - } - - public static final int ROTATION_VECTOR_Y_FIELD_NUMBER = 9; - private float rotationVectorY_; - /** - * optional float rotation_vector_y = 9; - */ - public float getRotationVectorY() { - return rotationVectorY_; - } - - public static final int ROTATION_VECTOR_Z_FIELD_NUMBER = 10; - private float rotationVectorZ_; - /** - * optional float rotation_vector_z = 10; - */ - public float getRotationVectorZ() { - return rotationVectorZ_; - } - - public static final int ROTATION_VECTOR_W_FIELD_NUMBER = 11; - private float rotationVectorW_; - /** - * optional float rotation_vector_w = 11; - */ - public float getRotationVectorW() { - return rotationVectorW_; - } - - public static final int STEP_COUNT_FIELD_NUMBER = 12; - private int stepCount_; - /** - *
-     * Integer
-     * 
- * - * optional int32 step_count = 12; - */ - public int getStepCount() { - return stepCount_; - } - - private byte memoizedIsInitialized = -1; - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) return true; - if (isInitialized == 0) return false; - - memoizedIsInitialized = 1; - return true; - } - - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - if (relativeTimestamp_ != 0L) { - output.writeInt64(1, relativeTimestamp_); - } - if (accX_ != 0F) { - output.writeFloat(2, accX_); - } - if (accY_ != 0F) { - output.writeFloat(3, accY_); - } - if (accZ_ != 0F) { - output.writeFloat(4, accZ_); - } - if (gyrX_ != 0F) { - output.writeFloat(5, gyrX_); - } - if (gyrY_ != 0F) { - output.writeFloat(6, gyrY_); - } - if (gyrZ_ != 0F) { - output.writeFloat(7, gyrZ_); - } - if (rotationVectorX_ != 0F) { - output.writeFloat(8, rotationVectorX_); - } - if (rotationVectorY_ != 0F) { - output.writeFloat(9, rotationVectorY_); - } - if (rotationVectorZ_ != 0F) { - output.writeFloat(10, rotationVectorZ_); - } - if (rotationVectorW_ != 0F) { - output.writeFloat(11, rotationVectorW_); - } - if (stepCount_ != 0) { - output.writeInt32(12, stepCount_); - } - } - - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) return size; - - size = 0; - if (relativeTimestamp_ != 0L) { - size += com.google.protobuf.CodedOutputStream - .computeInt64Size(1, relativeTimestamp_); - } - if (accX_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(2, accX_); - } - if (accY_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(3, accY_); - } - if (accZ_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(4, accZ_); - } - if (gyrX_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(5, gyrX_); - } - if (gyrY_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(6, gyrY_); - } - if (gyrZ_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(7, gyrZ_); - } - if (rotationVectorX_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(8, rotationVectorX_); - } - if (rotationVectorY_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(9, rotationVectorY_); - } - if (rotationVectorZ_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(10, rotationVectorZ_); - } - if (rotationVectorW_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(11, rotationVectorW_); - } - if (stepCount_ != 0) { - size += com.google.protobuf.CodedOutputStream - .computeInt32Size(12, stepCount_); - } - memoizedSize = size; - return size; - } - - private static final long serialVersionUID = 0L; - @Override - public boolean equals(final Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof Motion_Sample)) { - return super.equals(obj); - } - Motion_Sample other = (Motion_Sample) obj; - - boolean result = true; - result = result && (getRelativeTimestamp() - == other.getRelativeTimestamp()); - result = result && ( - Float.floatToIntBits(getAccX()) - == Float.floatToIntBits( - other.getAccX())); - result = result && ( - Float.floatToIntBits(getAccY()) - == Float.floatToIntBits( - other.getAccY())); - result = result && ( - Float.floatToIntBits(getAccZ()) - == Float.floatToIntBits( - other.getAccZ())); - result = result && ( - Float.floatToIntBits(getGyrX()) - == Float.floatToIntBits( - other.getGyrX())); - result = result && ( - Float.floatToIntBits(getGyrY()) - == Float.floatToIntBits( - other.getGyrY())); - result = result && ( - Float.floatToIntBits(getGyrZ()) - == Float.floatToIntBits( - other.getGyrZ())); - result = result && ( - Float.floatToIntBits(getRotationVectorX()) - == Float.floatToIntBits( - other.getRotationVectorX())); - result = result && ( - Float.floatToIntBits(getRotationVectorY()) - == Float.floatToIntBits( - other.getRotationVectorY())); - result = result && ( - Float.floatToIntBits(getRotationVectorZ()) - == Float.floatToIntBits( - other.getRotationVectorZ())); - result = result && ( - Float.floatToIntBits(getRotationVectorW()) - == Float.floatToIntBits( - other.getRotationVectorW())); - result = result && (getStepCount() - == other.getStepCount()); - return result; - } - - @Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptorForType().hashCode(); - hash = (37 * hash) + RELATIVE_TIMESTAMP_FIELD_NUMBER; - hash = (53 * hash) + com.google.protobuf.Internal.hashLong( - getRelativeTimestamp()); - hash = (37 * hash) + ACC_X_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getAccX()); - hash = (37 * hash) + ACC_Y_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getAccY()); - hash = (37 * hash) + ACC_Z_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getAccZ()); - hash = (37 * hash) + GYR_X_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getGyrX()); - hash = (37 * hash) + GYR_Y_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getGyrY()); - hash = (37 * hash) + GYR_Z_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getGyrZ()); - hash = (37 * hash) + ROTATION_VECTOR_X_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getRotationVectorX()); - hash = (37 * hash) + ROTATION_VECTOR_Y_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getRotationVectorY()); - hash = (37 * hash) + ROTATION_VECTOR_Z_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getRotationVectorZ()); - hash = (37 * hash) + ROTATION_VECTOR_W_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getRotationVectorW()); - hash = (37 * hash) + STEP_COUNT_FIELD_NUMBER; - hash = (53 * hash) + getStepCount(); - hash = (29 * hash) + unknownFields.hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static Motion_Sample parseFrom( - com.google.protobuf.ByteString data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static Motion_Sample parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static Motion_Sample parseFrom(byte[] data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static Motion_Sample parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static Motion_Sample parseFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input); - } - public static Motion_Sample parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input, extensionRegistry); - } - public static Motion_Sample parseDelimitedFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseDelimitedWithIOException(PARSER, input); - } - public static Motion_Sample parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - public static Motion_Sample parseFrom( - com.google.protobuf.CodedInputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input); - } - public static Motion_Sample parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public Builder newBuilderForType() { return newBuilder(); } - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - public static Builder newBuilder(Motion_Sample prototype) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() : new Builder().mergeFrom(this); - } - - @Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { - Builder builder = new Builder(parent); - return builder; - } - /** - * Protobuf type {@code Motion_Sample} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessageV3.Builder implements - // @@protoc_insertion_point(builder_implements:Motion_Sample) - Motion_SampleOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return Traj.internal_static_Motion_Sample_descriptor; - } - - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return Traj.internal_static_Motion_Sample_fieldAccessorTable - .ensureFieldAccessorsInitialized( - Motion_Sample.class, Builder.class); - } - - // Construct using Traj.Motion_Sample.newBuilder() - private Builder() { - maybeForceBuilderInitialization(); - } - - private Builder( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { - super(parent); - maybeForceBuilderInitialization(); - } - private void maybeForceBuilderInitialization() { - if (com.google.protobuf.GeneratedMessageV3 - .alwaysUseFieldBuilders) { - } - } - public Builder clear() { - super.clear(); - relativeTimestamp_ = 0L; - - accX_ = 0F; - - accY_ = 0F; - - accZ_ = 0F; - - gyrX_ = 0F; - - gyrY_ = 0F; - - gyrZ_ = 0F; - - rotationVectorX_ = 0F; - - rotationVectorY_ = 0F; - - rotationVectorZ_ = 0F; - - rotationVectorW_ = 0F; - - stepCount_ = 0; - - return this; - } - - public com.google.protobuf.Descriptors.Descriptor - getDescriptorForType() { - return Traj.internal_static_Motion_Sample_descriptor; - } - - public Motion_Sample getDefaultInstanceForType() { - return Motion_Sample.getDefaultInstance(); - } - - public Motion_Sample build() { - Motion_Sample result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - public Motion_Sample buildPartial() { - Motion_Sample result = new Motion_Sample(this); - result.relativeTimestamp_ = relativeTimestamp_; - result.accX_ = accX_; - result.accY_ = accY_; - result.accZ_ = accZ_; - result.gyrX_ = gyrX_; - result.gyrY_ = gyrY_; - result.gyrZ_ = gyrZ_; - result.rotationVectorX_ = rotationVectorX_; - result.rotationVectorY_ = rotationVectorY_; - result.rotationVectorZ_ = rotationVectorZ_; - result.rotationVectorW_ = rotationVectorW_; - result.stepCount_ = stepCount_; - onBuilt(); - return result; - } - - public Builder clone() { - return (Builder) super.clone(); - } - public Builder setField( - com.google.protobuf.Descriptors.FieldDescriptor field, - Object value) { - return (Builder) super.setField(field, value); - } - public Builder clearField( - com.google.protobuf.Descriptors.FieldDescriptor field) { - return (Builder) super.clearField(field); - } - public Builder clearOneof( - com.google.protobuf.Descriptors.OneofDescriptor oneof) { - return (Builder) super.clearOneof(oneof); - } - public Builder setRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - int index, Object value) { - return (Builder) super.setRepeatedField(field, index, value); - } - public Builder addRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - Object value) { - return (Builder) super.addRepeatedField(field, value); - } - public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof Motion_Sample) { - return mergeFrom((Motion_Sample)other); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom(Motion_Sample other) { - if (other == Motion_Sample.getDefaultInstance()) return this; - if (other.getRelativeTimestamp() != 0L) { - setRelativeTimestamp(other.getRelativeTimestamp()); - } - if (other.getAccX() != 0F) { - setAccX(other.getAccX()); - } - if (other.getAccY() != 0F) { - setAccY(other.getAccY()); - } - if (other.getAccZ() != 0F) { - setAccZ(other.getAccZ()); - } - if (other.getGyrX() != 0F) { - setGyrX(other.getGyrX()); - } - if (other.getGyrY() != 0F) { - setGyrY(other.getGyrY()); - } - if (other.getGyrZ() != 0F) { - setGyrZ(other.getGyrZ()); - } - if (other.getRotationVectorX() != 0F) { - setRotationVectorX(other.getRotationVectorX()); - } - if (other.getRotationVectorY() != 0F) { - setRotationVectorY(other.getRotationVectorY()); - } - if (other.getRotationVectorZ() != 0F) { - setRotationVectorZ(other.getRotationVectorZ()); - } - if (other.getRotationVectorW() != 0F) { - setRotationVectorW(other.getRotationVectorW()); - } - if (other.getStepCount() != 0) { - setStepCount(other.getStepCount()); - } - onChanged(); - return this; - } - - public final boolean isInitialized() { - return true; - } - - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - Motion_Sample parsedMessage = null; - try { - parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - parsedMessage = (Motion_Sample) e.getUnfinishedMessage(); - throw e.unwrapIOException(); - } finally { - if (parsedMessage != null) { - mergeFrom(parsedMessage); - } - } - return this; - } - - private long relativeTimestamp_ ; - /** - *
-       * milliseconds
-       * 
- * - * optional int64 relative_timestamp = 1; - */ - public long getRelativeTimestamp() { - return relativeTimestamp_; - } - /** - *
-       * milliseconds
-       * 
- * - * optional int64 relative_timestamp = 1; - */ - public Builder setRelativeTimestamp(long value) { - - relativeTimestamp_ = value; - onChanged(); - return this; - } - /** - *
-       * milliseconds
-       * 
- * - * optional int64 relative_timestamp = 1; - */ - public Builder clearRelativeTimestamp() { - - relativeTimestamp_ = 0L; - onChanged(); - return this; - } - - private float accX_ ; - /** - *
-       * m/s^2
-       * 
- * - * optional float acc_x = 2; - */ - public float getAccX() { - return accX_; - } - /** - *
-       * m/s^2
-       * 
- * - * optional float acc_x = 2; - */ - public Builder setAccX(float value) { - - accX_ = value; - onChanged(); - return this; - } - /** - *
-       * m/s^2
-       * 
- * - * optional float acc_x = 2; - */ - public Builder clearAccX() { - - accX_ = 0F; - onChanged(); - return this; - } - - private float accY_ ; - /** - * optional float acc_y = 3; - */ - public float getAccY() { - return accY_; - } - /** - * optional float acc_y = 3; - */ - public Builder setAccY(float value) { - - accY_ = value; - onChanged(); - return this; - } - /** - * optional float acc_y = 3; - */ - public Builder clearAccY() { - - accY_ = 0F; - onChanged(); - return this; - } - - private float accZ_ ; - /** - * optional float acc_z = 4; - */ - public float getAccZ() { - return accZ_; - } - /** - * optional float acc_z = 4; - */ - public Builder setAccZ(float value) { - - accZ_ = value; - onChanged(); - return this; - } - /** - * optional float acc_z = 4; - */ - public Builder clearAccZ() { - - accZ_ = 0F; - onChanged(); - return this; - } - - private float gyrX_ ; - /** - *
-       * radians/s
-       * 
- * - * optional float gyr_x = 5; - */ - public float getGyrX() { - return gyrX_; - } - /** - *
-       * radians/s
-       * 
- * - * optional float gyr_x = 5; - */ - public Builder setGyrX(float value) { - - gyrX_ = value; - onChanged(); - return this; - } - /** - *
-       * radians/s
-       * 
- * - * optional float gyr_x = 5; - */ - public Builder clearGyrX() { - - gyrX_ = 0F; - onChanged(); - return this; - } - - private float gyrY_ ; - /** - * optional float gyr_y = 6; - */ - public float getGyrY() { - return gyrY_; - } - /** - * optional float gyr_y = 6; - */ - public Builder setGyrY(float value) { - - gyrY_ = value; - onChanged(); - return this; - } - /** - * optional float gyr_y = 6; - */ - public Builder clearGyrY() { - - gyrY_ = 0F; - onChanged(); - return this; - } - - private float gyrZ_ ; - /** - * optional float gyr_z = 7; - */ - public float getGyrZ() { - return gyrZ_; - } - /** - * optional float gyr_z = 7; - */ - public Builder setGyrZ(float value) { - - gyrZ_ = value; - onChanged(); - return this; - } - /** - * optional float gyr_z = 7; - */ - public Builder clearGyrZ() { - - gyrZ_ = 0F; - onChanged(); - return this; - } - - private float rotationVectorX_ ; - /** - *
-       * unitless, 4 components should sum to ~1
-       * 
- * - * optional float rotation_vector_x = 8; - */ - public float getRotationVectorX() { - return rotationVectorX_; - } - /** - *
-       * unitless, 4 components should sum to ~1
-       * 
- * - * optional float rotation_vector_x = 8; - */ - public Builder setRotationVectorX(float value) { - - rotationVectorX_ = value; - onChanged(); - return this; - } - /** - *
-       * unitless, 4 components should sum to ~1
-       * 
- * - * optional float rotation_vector_x = 8; - */ - public Builder clearRotationVectorX() { - - rotationVectorX_ = 0F; - onChanged(); - return this; - } - - private float rotationVectorY_ ; - /** - * optional float rotation_vector_y = 9; - */ - public float getRotationVectorY() { - return rotationVectorY_; - } - /** - * optional float rotation_vector_y = 9; - */ - public Builder setRotationVectorY(float value) { - - rotationVectorY_ = value; - onChanged(); - return this; - } - /** - * optional float rotation_vector_y = 9; - */ - public Builder clearRotationVectorY() { - - rotationVectorY_ = 0F; - onChanged(); - return this; - } - - private float rotationVectorZ_ ; - /** - * optional float rotation_vector_z = 10; - */ - public float getRotationVectorZ() { - return rotationVectorZ_; - } - /** - * optional float rotation_vector_z = 10; - */ - public Builder setRotationVectorZ(float value) { - - rotationVectorZ_ = value; - onChanged(); - return this; - } - /** - * optional float rotation_vector_z = 10; - */ - public Builder clearRotationVectorZ() { - - rotationVectorZ_ = 0F; - onChanged(); - return this; - } - - private float rotationVectorW_ ; - /** - * optional float rotation_vector_w = 11; - */ - public float getRotationVectorW() { - return rotationVectorW_; - } - /** - * optional float rotation_vector_w = 11; - */ - public Builder setRotationVectorW(float value) { - - rotationVectorW_ = value; - onChanged(); - return this; - } - /** - * optional float rotation_vector_w = 11; - */ - public Builder clearRotationVectorW() { - - rotationVectorW_ = 0F; - onChanged(); - return this; - } - - private int stepCount_ ; - /** - *
-       * Integer
-       * 
- * - * optional int32 step_count = 12; - */ - public int getStepCount() { - return stepCount_; - } - /** - *
-       * Integer
-       * 
- * - * optional int32 step_count = 12; - */ - public Builder setStepCount(int value) { - - stepCount_ = value; - onChanged(); - return this; - } - /** - *
-       * Integer
-       * 
- * - * optional int32 step_count = 12; - */ - public Builder clearStepCount() { - - stepCount_ = 0; - onChanged(); - return this; - } - public final Builder setUnknownFields( - final com.google.protobuf.UnknownFieldSet unknownFields) { - return this; - } - - public final Builder mergeUnknownFields( - final com.google.protobuf.UnknownFieldSet unknownFields) { - return this; - } - - - // @@protoc_insertion_point(builder_scope:Motion_Sample) - } - - // @@protoc_insertion_point(class_scope:Motion_Sample) - private static final Motion_Sample DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new Motion_Sample(); - } - - public static Motion_Sample getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser - PARSER = new com.google.protobuf.AbstractParser() { - public Motion_Sample parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return new Motion_Sample(input, extensionRegistry); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - public Motion_Sample getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - - } - - public interface Position_SampleOrBuilder extends - // @@protoc_insertion_point(interface_extends:Position_Sample) - com.google.protobuf.MessageOrBuilder { - - /** - * optional int64 relative_timestamp = 1; - */ - long getRelativeTimestamp(); - - /** - *
-     * uT
-     * 
- * - * optional float mag_x = 2; - */ - float getMagX(); - - /** - * optional float mag_y = 3; - */ - float getMagY(); - - /** - * optional float mag_z = 4; - */ - float getMagZ(); - } - /** - * Protobuf type {@code Position_Sample} - */ - public static final class Position_Sample extends - com.google.protobuf.GeneratedMessageV3 implements - // @@protoc_insertion_point(message_implements:Position_Sample) - Position_SampleOrBuilder { - // Use Position_Sample.newBuilder() to construct. - private Position_Sample(com.google.protobuf.GeneratedMessageV3.Builder builder) { - super(builder); - } - private Position_Sample() { - relativeTimestamp_ = 0L; - magX_ = 0F; - magY_ = 0F; - magZ_ = 0F; - } - - @Override - public final com.google.protobuf.UnknownFieldSet - getUnknownFields() { - return com.google.protobuf.UnknownFieldSet.getDefaultInstance(); - } - private Position_Sample( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - this(); - int mutable_bitField0_ = 0; - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - default: { - if (!input.skipField(tag)) { - done = true; - } - break; - } - case 8: { - - relativeTimestamp_ = input.readInt64(); - break; - } - case 21: { - - magX_ = input.readFloat(); - break; - } - case 29: { - - magY_ = input.readFloat(); - break; - } - case 37: { - - magZ_ = input.readFloat(); - break; - } - } - } - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(this); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException( - e).setUnfinishedMessage(this); - } finally { - makeExtensionsImmutable(); - } - } - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return Traj.internal_static_Position_Sample_descriptor; - } - - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return Traj.internal_static_Position_Sample_fieldAccessorTable - .ensureFieldAccessorsInitialized( - Position_Sample.class, Builder.class); - } - - public static final int RELATIVE_TIMESTAMP_FIELD_NUMBER = 1; - private long relativeTimestamp_; - /** - * optional int64 relative_timestamp = 1; - */ - public long getRelativeTimestamp() { - return relativeTimestamp_; - } - - public static final int MAG_X_FIELD_NUMBER = 2; - private float magX_; - /** - *
-     * uT
-     * 
- * - * optional float mag_x = 2; - */ - public float getMagX() { - return magX_; - } - - public static final int MAG_Y_FIELD_NUMBER = 3; - private float magY_; - /** - * optional float mag_y = 3; - */ - public float getMagY() { - return magY_; - } - - public static final int MAG_Z_FIELD_NUMBER = 4; - private float magZ_; - /** - * optional float mag_z = 4; - */ - public float getMagZ() { - return magZ_; - } - - private byte memoizedIsInitialized = -1; - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) return true; - if (isInitialized == 0) return false; - - memoizedIsInitialized = 1; - return true; - } - - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - if (relativeTimestamp_ != 0L) { - output.writeInt64(1, relativeTimestamp_); - } - if (magX_ != 0F) { - output.writeFloat(2, magX_); - } - if (magY_ != 0F) { - output.writeFloat(3, magY_); - } - if (magZ_ != 0F) { - output.writeFloat(4, magZ_); - } - } - - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) return size; - - size = 0; - if (relativeTimestamp_ != 0L) { - size += com.google.protobuf.CodedOutputStream - .computeInt64Size(1, relativeTimestamp_); - } - if (magX_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(2, magX_); - } - if (magY_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(3, magY_); - } - if (magZ_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(4, magZ_); - } - memoizedSize = size; - return size; - } - - private static final long serialVersionUID = 0L; - @Override - public boolean equals(final Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof Position_Sample)) { - return super.equals(obj); - } - Position_Sample other = (Position_Sample) obj; - - boolean result = true; - result = result && (getRelativeTimestamp() - == other.getRelativeTimestamp()); - result = result && ( - Float.floatToIntBits(getMagX()) - == Float.floatToIntBits( - other.getMagX())); - result = result && ( - Float.floatToIntBits(getMagY()) - == Float.floatToIntBits( - other.getMagY())); - result = result && ( - Float.floatToIntBits(getMagZ()) - == Float.floatToIntBits( - other.getMagZ())); - return result; - } - - @Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptorForType().hashCode(); - hash = (37 * hash) + RELATIVE_TIMESTAMP_FIELD_NUMBER; - hash = (53 * hash) + com.google.protobuf.Internal.hashLong( - getRelativeTimestamp()); - hash = (37 * hash) + MAG_X_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getMagX()); - hash = (37 * hash) + MAG_Y_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getMagY()); - hash = (37 * hash) + MAG_Z_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getMagZ()); - hash = (29 * hash) + unknownFields.hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static Position_Sample parseFrom( - com.google.protobuf.ByteString data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static Position_Sample parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static Position_Sample parseFrom(byte[] data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static Position_Sample parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static Position_Sample parseFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input); - } - public static Position_Sample parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input, extensionRegistry); - } - public static Position_Sample parseDelimitedFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseDelimitedWithIOException(PARSER, input); - } - public static Position_Sample parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - public static Position_Sample parseFrom( - com.google.protobuf.CodedInputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input); - } - public static Position_Sample parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public Builder newBuilderForType() { return newBuilder(); } - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - public static Builder newBuilder(Position_Sample prototype) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() : new Builder().mergeFrom(this); - } - - @Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { - Builder builder = new Builder(parent); - return builder; - } - /** - * Protobuf type {@code Position_Sample} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessageV3.Builder implements - // @@protoc_insertion_point(builder_implements:Position_Sample) - Position_SampleOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return Traj.internal_static_Position_Sample_descriptor; - } - - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return Traj.internal_static_Position_Sample_fieldAccessorTable - .ensureFieldAccessorsInitialized( - Position_Sample.class, Builder.class); - } - - // Construct using Traj.Position_Sample.newBuilder() - private Builder() { - maybeForceBuilderInitialization(); - } - - private Builder( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { - super(parent); - maybeForceBuilderInitialization(); - } - private void maybeForceBuilderInitialization() { - if (com.google.protobuf.GeneratedMessageV3 - .alwaysUseFieldBuilders) { - } - } - public Builder clear() { - super.clear(); - relativeTimestamp_ = 0L; - - magX_ = 0F; - - magY_ = 0F; - - magZ_ = 0F; - - return this; - } - - public com.google.protobuf.Descriptors.Descriptor - getDescriptorForType() { - return Traj.internal_static_Position_Sample_descriptor; - } - - public Position_Sample getDefaultInstanceForType() { - return Position_Sample.getDefaultInstance(); - } - - public Position_Sample build() { - Position_Sample result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - public Position_Sample buildPartial() { - Position_Sample result = new Position_Sample(this); - result.relativeTimestamp_ = relativeTimestamp_; - result.magX_ = magX_; - result.magY_ = magY_; - result.magZ_ = magZ_; - onBuilt(); - return result; - } - - public Builder clone() { - return (Builder) super.clone(); - } - public Builder setField( - com.google.protobuf.Descriptors.FieldDescriptor field, - Object value) { - return (Builder) super.setField(field, value); - } - public Builder clearField( - com.google.protobuf.Descriptors.FieldDescriptor field) { - return (Builder) super.clearField(field); - } - public Builder clearOneof( - com.google.protobuf.Descriptors.OneofDescriptor oneof) { - return (Builder) super.clearOneof(oneof); - } - public Builder setRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - int index, Object value) { - return (Builder) super.setRepeatedField(field, index, value); - } - public Builder addRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - Object value) { - return (Builder) super.addRepeatedField(field, value); - } - public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof Position_Sample) { - return mergeFrom((Position_Sample)other); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom(Position_Sample other) { - if (other == Position_Sample.getDefaultInstance()) return this; - if (other.getRelativeTimestamp() != 0L) { - setRelativeTimestamp(other.getRelativeTimestamp()); - } - if (other.getMagX() != 0F) { - setMagX(other.getMagX()); - } - if (other.getMagY() != 0F) { - setMagY(other.getMagY()); - } - if (other.getMagZ() != 0F) { - setMagZ(other.getMagZ()); - } - onChanged(); - return this; - } - - public final boolean isInitialized() { - return true; - } - - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - Position_Sample parsedMessage = null; - try { - parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - parsedMessage = (Position_Sample) e.getUnfinishedMessage(); - throw e.unwrapIOException(); - } finally { - if (parsedMessage != null) { - mergeFrom(parsedMessage); - } - } - return this; - } - - private long relativeTimestamp_ ; - /** - * optional int64 relative_timestamp = 1; - */ - public long getRelativeTimestamp() { - return relativeTimestamp_; - } - /** - * optional int64 relative_timestamp = 1; - */ - public Builder setRelativeTimestamp(long value) { - - relativeTimestamp_ = value; - onChanged(); - return this; - } - /** - * optional int64 relative_timestamp = 1; - */ - public Builder clearRelativeTimestamp() { - - relativeTimestamp_ = 0L; - onChanged(); - return this; - } - - private float magX_ ; - /** - *
-       * uT
-       * 
- * - * optional float mag_x = 2; - */ - public float getMagX() { - return magX_; - } - /** - *
-       * uT
-       * 
- * - * optional float mag_x = 2; - */ - public Builder setMagX(float value) { - - magX_ = value; - onChanged(); - return this; - } - /** - *
-       * uT
-       * 
- * - * optional float mag_x = 2; - */ - public Builder clearMagX() { - - magX_ = 0F; - onChanged(); - return this; - } - - private float magY_ ; - /** - * optional float mag_y = 3; - */ - public float getMagY() { - return magY_; - } - /** - * optional float mag_y = 3; - */ - public Builder setMagY(float value) { - - magY_ = value; - onChanged(); - return this; - } - /** - * optional float mag_y = 3; - */ - public Builder clearMagY() { - - magY_ = 0F; - onChanged(); - return this; - } - - private float magZ_ ; - /** - * optional float mag_z = 4; - */ - public float getMagZ() { - return magZ_; - } - /** - * optional float mag_z = 4; - */ - public Builder setMagZ(float value) { - - magZ_ = value; - onChanged(); - return this; - } - /** - * optional float mag_z = 4; - */ - public Builder clearMagZ() { - - magZ_ = 0F; - onChanged(); - return this; - } - public final Builder setUnknownFields( - final com.google.protobuf.UnknownFieldSet unknownFields) { - return this; - } - - public final Builder mergeUnknownFields( - final com.google.protobuf.UnknownFieldSet unknownFields) { - return this; - } - - - // @@protoc_insertion_point(builder_scope:Position_Sample) - } - - // @@protoc_insertion_point(class_scope:Position_Sample) - private static final Position_Sample DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new Position_Sample(); - } - - public static Position_Sample getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser - PARSER = new com.google.protobuf.AbstractParser() { - public Position_Sample parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return new Position_Sample(input, extensionRegistry); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - public Position_Sample getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - - } - - public interface Pressure_SampleOrBuilder extends - // @@protoc_insertion_point(interface_extends:Pressure_Sample) - com.google.protobuf.MessageOrBuilder { - - /** - * optional int64 relative_timestamp = 1; - */ - long getRelativeTimestamp(); - - /** - *
-     * mbar
-     * 
- * - * optional float pressure = 2; - */ - float getPressure(); - } - /** - * Protobuf type {@code Pressure_Sample} - */ - public static final class Pressure_Sample extends - com.google.protobuf.GeneratedMessageV3 implements - // @@protoc_insertion_point(message_implements:Pressure_Sample) - Pressure_SampleOrBuilder { - // Use Pressure_Sample.newBuilder() to construct. - private Pressure_Sample(com.google.protobuf.GeneratedMessageV3.Builder builder) { - super(builder); - } - private Pressure_Sample() { - relativeTimestamp_ = 0L; - pressure_ = 0F; - } - - @Override - public final com.google.protobuf.UnknownFieldSet - getUnknownFields() { - return com.google.protobuf.UnknownFieldSet.getDefaultInstance(); - } - private Pressure_Sample( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - this(); - int mutable_bitField0_ = 0; - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - default: { - if (!input.skipField(tag)) { - done = true; - } - break; - } - case 8: { - - relativeTimestamp_ = input.readInt64(); - break; - } - case 21: { - - pressure_ = input.readFloat(); - break; - } - } - } - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(this); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException( - e).setUnfinishedMessage(this); - } finally { - makeExtensionsImmutable(); - } - } - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return Traj.internal_static_Pressure_Sample_descriptor; - } - - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return Traj.internal_static_Pressure_Sample_fieldAccessorTable - .ensureFieldAccessorsInitialized( - Pressure_Sample.class, Builder.class); - } - - public static final int RELATIVE_TIMESTAMP_FIELD_NUMBER = 1; - private long relativeTimestamp_; - /** - * optional int64 relative_timestamp = 1; - */ - public long getRelativeTimestamp() { - return relativeTimestamp_; - } - - public static final int PRESSURE_FIELD_NUMBER = 2; - private float pressure_; - /** - *
-     * mbar
-     * 
- * - * optional float pressure = 2; - */ - public float getPressure() { - return pressure_; - } - - private byte memoizedIsInitialized = -1; - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) return true; - if (isInitialized == 0) return false; - - memoizedIsInitialized = 1; - return true; - } - - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - if (relativeTimestamp_ != 0L) { - output.writeInt64(1, relativeTimestamp_); - } - if (pressure_ != 0F) { - output.writeFloat(2, pressure_); - } - } - - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) return size; - - size = 0; - if (relativeTimestamp_ != 0L) { - size += com.google.protobuf.CodedOutputStream - .computeInt64Size(1, relativeTimestamp_); - } - if (pressure_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(2, pressure_); - } - memoizedSize = size; - return size; - } - - private static final long serialVersionUID = 0L; - @Override - public boolean equals(final Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof Pressure_Sample)) { - return super.equals(obj); - } - Pressure_Sample other = (Pressure_Sample) obj; - - boolean result = true; - result = result && (getRelativeTimestamp() - == other.getRelativeTimestamp()); - result = result && ( - Float.floatToIntBits(getPressure()) - == Float.floatToIntBits( - other.getPressure())); - return result; - } - - @Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptorForType().hashCode(); - hash = (37 * hash) + RELATIVE_TIMESTAMP_FIELD_NUMBER; - hash = (53 * hash) + com.google.protobuf.Internal.hashLong( - getRelativeTimestamp()); - hash = (37 * hash) + PRESSURE_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getPressure()); - hash = (29 * hash) + unknownFields.hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static Pressure_Sample parseFrom( - com.google.protobuf.ByteString data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static Pressure_Sample parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static Pressure_Sample parseFrom(byte[] data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static Pressure_Sample parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static Pressure_Sample parseFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input); - } - public static Pressure_Sample parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input, extensionRegistry); - } - public static Pressure_Sample parseDelimitedFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseDelimitedWithIOException(PARSER, input); - } - public static Pressure_Sample parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - public static Pressure_Sample parseFrom( - com.google.protobuf.CodedInputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input); - } - public static Pressure_Sample parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public Builder newBuilderForType() { return newBuilder(); } - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - public static Builder newBuilder(Pressure_Sample prototype) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() : new Builder().mergeFrom(this); - } - - @Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { - Builder builder = new Builder(parent); - return builder; - } - /** - * Protobuf type {@code Pressure_Sample} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessageV3.Builder implements - // @@protoc_insertion_point(builder_implements:Pressure_Sample) - Pressure_SampleOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return Traj.internal_static_Pressure_Sample_descriptor; - } - - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return Traj.internal_static_Pressure_Sample_fieldAccessorTable - .ensureFieldAccessorsInitialized( - Pressure_Sample.class, Builder.class); - } - - // Construct using Traj.Pressure_Sample.newBuilder() - private Builder() { - maybeForceBuilderInitialization(); - } - - private Builder( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { - super(parent); - maybeForceBuilderInitialization(); - } - private void maybeForceBuilderInitialization() { - if (com.google.protobuf.GeneratedMessageV3 - .alwaysUseFieldBuilders) { - } - } - public Builder clear() { - super.clear(); - relativeTimestamp_ = 0L; - - pressure_ = 0F; - - return this; - } - - public com.google.protobuf.Descriptors.Descriptor - getDescriptorForType() { - return Traj.internal_static_Pressure_Sample_descriptor; - } - - public Pressure_Sample getDefaultInstanceForType() { - return Pressure_Sample.getDefaultInstance(); - } - - public Pressure_Sample build() { - Pressure_Sample result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - public Pressure_Sample buildPartial() { - Pressure_Sample result = new Pressure_Sample(this); - result.relativeTimestamp_ = relativeTimestamp_; - result.pressure_ = pressure_; - onBuilt(); - return result; - } - - public Builder clone() { - return (Builder) super.clone(); - } - public Builder setField( - com.google.protobuf.Descriptors.FieldDescriptor field, - Object value) { - return (Builder) super.setField(field, value); - } - public Builder clearField( - com.google.protobuf.Descriptors.FieldDescriptor field) { - return (Builder) super.clearField(field); - } - public Builder clearOneof( - com.google.protobuf.Descriptors.OneofDescriptor oneof) { - return (Builder) super.clearOneof(oneof); - } - public Builder setRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - int index, Object value) { - return (Builder) super.setRepeatedField(field, index, value); - } - public Builder addRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - Object value) { - return (Builder) super.addRepeatedField(field, value); - } - public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof Pressure_Sample) { - return mergeFrom((Pressure_Sample)other); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom(Pressure_Sample other) { - if (other == Pressure_Sample.getDefaultInstance()) return this; - if (other.getRelativeTimestamp() != 0L) { - setRelativeTimestamp(other.getRelativeTimestamp()); - } - if (other.getPressure() != 0F) { - setPressure(other.getPressure()); - } - onChanged(); - return this; - } - - public final boolean isInitialized() { - return true; - } - - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - Pressure_Sample parsedMessage = null; - try { - parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - parsedMessage = (Pressure_Sample) e.getUnfinishedMessage(); - throw e.unwrapIOException(); - } finally { - if (parsedMessage != null) { - mergeFrom(parsedMessage); - } - } - return this; - } - - private long relativeTimestamp_ ; - /** - * optional int64 relative_timestamp = 1; - */ - public long getRelativeTimestamp() { - return relativeTimestamp_; - } - /** - * optional int64 relative_timestamp = 1; - */ - public Builder setRelativeTimestamp(long value) { - - relativeTimestamp_ = value; - onChanged(); - return this; - } - /** - * optional int64 relative_timestamp = 1; - */ - public Builder clearRelativeTimestamp() { - - relativeTimestamp_ = 0L; - onChanged(); - return this; - } - - private float pressure_ ; - /** - *
-       * mbar
-       * 
- * - * optional float pressure = 2; - */ - public float getPressure() { - return pressure_; - } - /** - *
-       * mbar
-       * 
- * - * optional float pressure = 2; - */ - public Builder setPressure(float value) { - - pressure_ = value; - onChanged(); - return this; - } - /** - *
-       * mbar
-       * 
- * - * optional float pressure = 2; - */ - public Builder clearPressure() { - - pressure_ = 0F; - onChanged(); - return this; - } - public final Builder setUnknownFields( - final com.google.protobuf.UnknownFieldSet unknownFields) { - return this; - } - - public final Builder mergeUnknownFields( - final com.google.protobuf.UnknownFieldSet unknownFields) { - return this; - } - - - // @@protoc_insertion_point(builder_scope:Pressure_Sample) - } - - // @@protoc_insertion_point(class_scope:Pressure_Sample) - private static final Pressure_Sample DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new Pressure_Sample(); - } - - public static Pressure_Sample getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser - PARSER = new com.google.protobuf.AbstractParser() { - public Pressure_Sample parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return new Pressure_Sample(input, extensionRegistry); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - public Pressure_Sample getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - - } - - public interface Light_SampleOrBuilder extends - // @@protoc_insertion_point(interface_extends:Light_Sample) - com.google.protobuf.MessageOrBuilder { - - /** - * optional int64 relative_timestamp = 1; - */ - long getRelativeTimestamp(); - - /** - *
-     * lux
-     * 
- * - * optional float light = 2; - */ - float getLight(); - } - /** - * Protobuf type {@code Light_Sample} - */ - public static final class Light_Sample extends - com.google.protobuf.GeneratedMessageV3 implements - // @@protoc_insertion_point(message_implements:Light_Sample) - Light_SampleOrBuilder { - // Use Light_Sample.newBuilder() to construct. - private Light_Sample(com.google.protobuf.GeneratedMessageV3.Builder builder) { - super(builder); - } - private Light_Sample() { - relativeTimestamp_ = 0L; - light_ = 0F; - } - - @Override - public final com.google.protobuf.UnknownFieldSet - getUnknownFields() { - return com.google.protobuf.UnknownFieldSet.getDefaultInstance(); - } - private Light_Sample( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - this(); - int mutable_bitField0_ = 0; - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - default: { - if (!input.skipField(tag)) { - done = true; - } - break; - } - case 8: { - - relativeTimestamp_ = input.readInt64(); - break; - } - case 21: { - - light_ = input.readFloat(); - break; - } - } - } - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(this); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException( - e).setUnfinishedMessage(this); - } finally { - makeExtensionsImmutable(); - } - } - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return Traj.internal_static_Light_Sample_descriptor; - } - - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return Traj.internal_static_Light_Sample_fieldAccessorTable - .ensureFieldAccessorsInitialized( - Light_Sample.class, Builder.class); - } - - public static final int RELATIVE_TIMESTAMP_FIELD_NUMBER = 1; - private long relativeTimestamp_; - /** - * optional int64 relative_timestamp = 1; - */ - public long getRelativeTimestamp() { - return relativeTimestamp_; - } - - public static final int LIGHT_FIELD_NUMBER = 2; - private float light_; - /** - *
-     * lux
-     * 
- * - * optional float light = 2; - */ - public float getLight() { - return light_; - } - - private byte memoizedIsInitialized = -1; - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) return true; - if (isInitialized == 0) return false; - - memoizedIsInitialized = 1; - return true; - } - - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - if (relativeTimestamp_ != 0L) { - output.writeInt64(1, relativeTimestamp_); - } - if (light_ != 0F) { - output.writeFloat(2, light_); - } - } - - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) return size; - - size = 0; - if (relativeTimestamp_ != 0L) { - size += com.google.protobuf.CodedOutputStream - .computeInt64Size(1, relativeTimestamp_); - } - if (light_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(2, light_); - } - memoizedSize = size; - return size; - } - - private static final long serialVersionUID = 0L; - @Override - public boolean equals(final Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof Light_Sample)) { - return super.equals(obj); - } - Light_Sample other = (Light_Sample) obj; - - boolean result = true; - result = result && (getRelativeTimestamp() - == other.getRelativeTimestamp()); - result = result && ( - Float.floatToIntBits(getLight()) - == Float.floatToIntBits( - other.getLight())); - return result; - } - - @Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptorForType().hashCode(); - hash = (37 * hash) + RELATIVE_TIMESTAMP_FIELD_NUMBER; - hash = (53 * hash) + com.google.protobuf.Internal.hashLong( - getRelativeTimestamp()); - hash = (37 * hash) + LIGHT_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getLight()); - hash = (29 * hash) + unknownFields.hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static Light_Sample parseFrom( - com.google.protobuf.ByteString data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static Light_Sample parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static Light_Sample parseFrom(byte[] data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static Light_Sample parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static Light_Sample parseFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input); - } - public static Light_Sample parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input, extensionRegistry); - } - public static Light_Sample parseDelimitedFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseDelimitedWithIOException(PARSER, input); - } - public static Light_Sample parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - public static Light_Sample parseFrom( - com.google.protobuf.CodedInputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input); - } - public static Light_Sample parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public Builder newBuilderForType() { return newBuilder(); } - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - public static Builder newBuilder(Light_Sample prototype) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() : new Builder().mergeFrom(this); - } - - @Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { - Builder builder = new Builder(parent); - return builder; - } - /** - * Protobuf type {@code Light_Sample} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessageV3.Builder implements - // @@protoc_insertion_point(builder_implements:Light_Sample) - Light_SampleOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return Traj.internal_static_Light_Sample_descriptor; - } - - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return Traj.internal_static_Light_Sample_fieldAccessorTable - .ensureFieldAccessorsInitialized( - Light_Sample.class, Builder.class); - } - - // Construct using Traj.Light_Sample.newBuilder() - private Builder() { - maybeForceBuilderInitialization(); - } - - private Builder( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { - super(parent); - maybeForceBuilderInitialization(); - } - private void maybeForceBuilderInitialization() { - if (com.google.protobuf.GeneratedMessageV3 - .alwaysUseFieldBuilders) { - } - } - public Builder clear() { - super.clear(); - relativeTimestamp_ = 0L; - - light_ = 0F; - - return this; - } - - public com.google.protobuf.Descriptors.Descriptor - getDescriptorForType() { - return Traj.internal_static_Light_Sample_descriptor; - } - - public Light_Sample getDefaultInstanceForType() { - return Light_Sample.getDefaultInstance(); - } - - public Light_Sample build() { - Light_Sample result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - public Light_Sample buildPartial() { - Light_Sample result = new Light_Sample(this); - result.relativeTimestamp_ = relativeTimestamp_; - result.light_ = light_; - onBuilt(); - return result; - } - - public Builder clone() { - return (Builder) super.clone(); - } - public Builder setField( - com.google.protobuf.Descriptors.FieldDescriptor field, - Object value) { - return (Builder) super.setField(field, value); - } - public Builder clearField( - com.google.protobuf.Descriptors.FieldDescriptor field) { - return (Builder) super.clearField(field); - } - public Builder clearOneof( - com.google.protobuf.Descriptors.OneofDescriptor oneof) { - return (Builder) super.clearOneof(oneof); - } - public Builder setRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - int index, Object value) { - return (Builder) super.setRepeatedField(field, index, value); - } - public Builder addRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - Object value) { - return (Builder) super.addRepeatedField(field, value); - } - public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof Light_Sample) { - return mergeFrom((Light_Sample)other); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom(Light_Sample other) { - if (other == Light_Sample.getDefaultInstance()) return this; - if (other.getRelativeTimestamp() != 0L) { - setRelativeTimestamp(other.getRelativeTimestamp()); - } - if (other.getLight() != 0F) { - setLight(other.getLight()); - } - onChanged(); - return this; - } - - public final boolean isInitialized() { - return true; - } - - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - Light_Sample parsedMessage = null; - try { - parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - parsedMessage = (Light_Sample) e.getUnfinishedMessage(); - throw e.unwrapIOException(); - } finally { - if (parsedMessage != null) { - mergeFrom(parsedMessage); - } - } - return this; - } - - private long relativeTimestamp_ ; - /** - * optional int64 relative_timestamp = 1; - */ - public long getRelativeTimestamp() { - return relativeTimestamp_; - } - /** - * optional int64 relative_timestamp = 1; - */ - public Builder setRelativeTimestamp(long value) { - - relativeTimestamp_ = value; - onChanged(); - return this; - } - /** - * optional int64 relative_timestamp = 1; - */ - public Builder clearRelativeTimestamp() { - - relativeTimestamp_ = 0L; - onChanged(); - return this; - } - - private float light_ ; - /** - *
-       * lux
-       * 
- * - * optional float light = 2; - */ - public float getLight() { - return light_; - } - /** - *
-       * lux
-       * 
- * - * optional float light = 2; - */ - public Builder setLight(float value) { - - light_ = value; - onChanged(); - return this; - } - /** - *
-       * lux
-       * 
- * - * optional float light = 2; - */ - public Builder clearLight() { - - light_ = 0F; - onChanged(); - return this; - } - public final Builder setUnknownFields( - final com.google.protobuf.UnknownFieldSet unknownFields) { - return this; - } - - public final Builder mergeUnknownFields( - final com.google.protobuf.UnknownFieldSet unknownFields) { - return this; - } - - - // @@protoc_insertion_point(builder_scope:Light_Sample) - } - - // @@protoc_insertion_point(class_scope:Light_Sample) - private static final Light_Sample DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new Light_Sample(); - } - - public static Light_Sample getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser - PARSER = new com.google.protobuf.AbstractParser() { - public Light_Sample parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return new Light_Sample(input, extensionRegistry); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - public Light_Sample getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - - } - - public interface GNSS_SampleOrBuilder extends - // @@protoc_insertion_point(interface_extends:GNSS_Sample) - com.google.protobuf.MessageOrBuilder { - - /** - * optional int64 relative_timestamp = 1; - */ - long getRelativeTimestamp(); - - /** - *
-     * degrees (minimum 6 significant figures)
-     * latitude between -90 and 90
-     * 
- * - * optional float latitude = 2; - */ - float getLatitude(); - - /** - *
-     * longitude between -180 and 180
-     * 
- * - * optional float longitude = 3; - */ - float getLongitude(); - - /** - *
-     *metres
-     * 
- * - * optional float altitude = 4; - */ - float getAltitude(); - - /** - *
-     * metres
-     * 
- * - * optional float accuracy = 5; - */ - float getAccuracy(); - - /** - *
-     * m/s
-     * 
- * - * optional float speed = 6; - */ - float getSpeed(); - - /** - *
-     * e.g 'gps' or 'network'
-     * 
- * - * optional string provider = 7; - */ - String getProvider(); - /** - *
-     * e.g 'gps' or 'network'
-     * 
- * - * optional string provider = 7; - */ - com.google.protobuf.ByteString - getProviderBytes(); - } - /** - * Protobuf type {@code GNSS_Sample} - */ - public static final class GNSS_Sample extends - com.google.protobuf.GeneratedMessageV3 implements - // @@protoc_insertion_point(message_implements:GNSS_Sample) - GNSS_SampleOrBuilder { - // Use GNSS_Sample.newBuilder() to construct. - private GNSS_Sample(com.google.protobuf.GeneratedMessageV3.Builder builder) { - super(builder); - } - private GNSS_Sample() { - relativeTimestamp_ = 0L; - latitude_ = 0F; - longitude_ = 0F; - altitude_ = 0F; - accuracy_ = 0F; - speed_ = 0F; - provider_ = ""; - } - - @Override - public final com.google.protobuf.UnknownFieldSet - getUnknownFields() { - return com.google.protobuf.UnknownFieldSet.getDefaultInstance(); - } - private GNSS_Sample( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - this(); - int mutable_bitField0_ = 0; - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - default: { - if (!input.skipField(tag)) { - done = true; - } - break; - } - case 8: { - - relativeTimestamp_ = input.readInt64(); - break; - } - case 21: { - - latitude_ = input.readFloat(); - break; - } - case 29: { - - longitude_ = input.readFloat(); - break; - } - case 37: { - - altitude_ = input.readFloat(); - break; - } - case 45: { - - accuracy_ = input.readFloat(); - break; - } - case 53: { - - speed_ = input.readFloat(); - break; - } - case 58: { - String s = input.readStringRequireUtf8(); - - provider_ = s; - break; - } - } - } - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(this); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException( - e).setUnfinishedMessage(this); - } finally { - makeExtensionsImmutable(); - } - } - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return Traj.internal_static_GNSS_Sample_descriptor; - } - - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return Traj.internal_static_GNSS_Sample_fieldAccessorTable - .ensureFieldAccessorsInitialized( - GNSS_Sample.class, Builder.class); - } - - public static final int RELATIVE_TIMESTAMP_FIELD_NUMBER = 1; - private long relativeTimestamp_; - /** - * optional int64 relative_timestamp = 1; - */ - public long getRelativeTimestamp() { - return relativeTimestamp_; - } - - public static final int LATITUDE_FIELD_NUMBER = 2; - private float latitude_; - /** - *
-     * degrees (minimum 6 significant figures)
-     * latitude between -90 and 90
-     * 
- * - * optional float latitude = 2; - */ - public float getLatitude() { - return latitude_; - } - - public static final int LONGITUDE_FIELD_NUMBER = 3; - private float longitude_; - /** - *
-     * longitude between -180 and 180
-     * 
- * - * optional float longitude = 3; - */ - public float getLongitude() { - return longitude_; - } - - public static final int ALTITUDE_FIELD_NUMBER = 4; - private float altitude_; - /** - *
-     *metres
-     * 
- * - * optional float altitude = 4; - */ - public float getAltitude() { - return altitude_; - } - - public static final int ACCURACY_FIELD_NUMBER = 5; - private float accuracy_; - /** - *
-     * metres
-     * 
- * - * optional float accuracy = 5; - */ - public float getAccuracy() { - return accuracy_; - } - - public static final int SPEED_FIELD_NUMBER = 6; - private float speed_; - /** - *
-     * m/s
-     * 
- * - * optional float speed = 6; - */ - public float getSpeed() { - return speed_; - } - - public static final int PROVIDER_FIELD_NUMBER = 7; - private volatile Object provider_; - /** - *
-     * e.g 'gps' or 'network'
-     * 
- * - * optional string provider = 7; - */ - public String getProvider() { - Object ref = provider_; - if (ref instanceof String) { - return (String) ref; - } else { - com.google.protobuf.ByteString bs = - (com.google.protobuf.ByteString) ref; - String s = bs.toStringUtf8(); - provider_ = s; - return s; - } - } - /** - *
-     * e.g 'gps' or 'network'
-     * 
- * - * optional string provider = 7; - */ - public com.google.protobuf.ByteString - getProviderBytes() { - Object ref = provider_; - if (ref instanceof String) { - com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8( - (String) ref); - provider_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - - private byte memoizedIsInitialized = -1; - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) return true; - if (isInitialized == 0) return false; - - memoizedIsInitialized = 1; - return true; - } - - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - if (relativeTimestamp_ != 0L) { - output.writeInt64(1, relativeTimestamp_); - } - if (latitude_ != 0F) { - output.writeFloat(2, latitude_); - } - if (longitude_ != 0F) { - output.writeFloat(3, longitude_); - } - if (altitude_ != 0F) { - output.writeFloat(4, altitude_); - } - if (accuracy_ != 0F) { - output.writeFloat(5, accuracy_); - } - if (speed_ != 0F) { - output.writeFloat(6, speed_); - } - if (!getProviderBytes().isEmpty()) { - com.google.protobuf.GeneratedMessageV3.writeString(output, 7, provider_); - } - } - - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) return size; - - size = 0; - if (relativeTimestamp_ != 0L) { - size += com.google.protobuf.CodedOutputStream - .computeInt64Size(1, relativeTimestamp_); - } - if (latitude_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(2, latitude_); - } - if (longitude_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(3, longitude_); - } - if (altitude_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(4, altitude_); - } - if (accuracy_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(5, accuracy_); - } - if (speed_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(6, speed_); - } - if (!getProviderBytes().isEmpty()) { - size += com.google.protobuf.GeneratedMessageV3.computeStringSize(7, provider_); - } - memoizedSize = size; - return size; - } - - private static final long serialVersionUID = 0L; - @Override - public boolean equals(final Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof GNSS_Sample)) { - return super.equals(obj); - } - GNSS_Sample other = (GNSS_Sample) obj; - - boolean result = true; - result = result && (getRelativeTimestamp() - == other.getRelativeTimestamp()); - result = result && ( - Float.floatToIntBits(getLatitude()) - == Float.floatToIntBits( - other.getLatitude())); - result = result && ( - Float.floatToIntBits(getLongitude()) - == Float.floatToIntBits( - other.getLongitude())); - result = result && ( - Float.floatToIntBits(getAltitude()) - == Float.floatToIntBits( - other.getAltitude())); - result = result && ( - Float.floatToIntBits(getAccuracy()) - == Float.floatToIntBits( - other.getAccuracy())); - result = result && ( - Float.floatToIntBits(getSpeed()) - == Float.floatToIntBits( - other.getSpeed())); - result = result && getProvider() - .equals(other.getProvider()); - return result; - } - - @Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptorForType().hashCode(); - hash = (37 * hash) + RELATIVE_TIMESTAMP_FIELD_NUMBER; - hash = (53 * hash) + com.google.protobuf.Internal.hashLong( - getRelativeTimestamp()); - hash = (37 * hash) + LATITUDE_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getLatitude()); - hash = (37 * hash) + LONGITUDE_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getLongitude()); - hash = (37 * hash) + ALTITUDE_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getAltitude()); - hash = (37 * hash) + ACCURACY_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getAccuracy()); - hash = (37 * hash) + SPEED_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getSpeed()); - hash = (37 * hash) + PROVIDER_FIELD_NUMBER; - hash = (53 * hash) + getProvider().hashCode(); - hash = (29 * hash) + unknownFields.hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static GNSS_Sample parseFrom( - com.google.protobuf.ByteString data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static GNSS_Sample parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static GNSS_Sample parseFrom(byte[] data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static GNSS_Sample parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static GNSS_Sample parseFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input); - } - public static GNSS_Sample parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input, extensionRegistry); - } - public static GNSS_Sample parseDelimitedFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseDelimitedWithIOException(PARSER, input); - } - public static GNSS_Sample parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - public static GNSS_Sample parseFrom( - com.google.protobuf.CodedInputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input); - } - public static GNSS_Sample parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public Builder newBuilderForType() { return newBuilder(); } - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - public static Builder newBuilder(GNSS_Sample prototype) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() : new Builder().mergeFrom(this); - } - - @Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { - Builder builder = new Builder(parent); - return builder; - } - /** - * Protobuf type {@code GNSS_Sample} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessageV3.Builder implements - // @@protoc_insertion_point(builder_implements:GNSS_Sample) - GNSS_SampleOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return Traj.internal_static_GNSS_Sample_descriptor; - } - - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return Traj.internal_static_GNSS_Sample_fieldAccessorTable - .ensureFieldAccessorsInitialized( - GNSS_Sample.class, Builder.class); - } - - // Construct using Traj.GNSS_Sample.newBuilder() - private Builder() { - maybeForceBuilderInitialization(); - } - - private Builder( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { - super(parent); - maybeForceBuilderInitialization(); - } - private void maybeForceBuilderInitialization() { - if (com.google.protobuf.GeneratedMessageV3 - .alwaysUseFieldBuilders) { - } - } - public Builder clear() { - super.clear(); - relativeTimestamp_ = 0L; - - latitude_ = 0F; - - longitude_ = 0F; - - altitude_ = 0F; - - accuracy_ = 0F; - - speed_ = 0F; - - provider_ = ""; - - return this; - } - - public com.google.protobuf.Descriptors.Descriptor - getDescriptorForType() { - return Traj.internal_static_GNSS_Sample_descriptor; - } - - public GNSS_Sample getDefaultInstanceForType() { - return GNSS_Sample.getDefaultInstance(); - } - - public GNSS_Sample build() { - GNSS_Sample result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - public GNSS_Sample buildPartial() { - GNSS_Sample result = new GNSS_Sample(this); - result.relativeTimestamp_ = relativeTimestamp_; - result.latitude_ = latitude_; - result.longitude_ = longitude_; - result.altitude_ = altitude_; - result.accuracy_ = accuracy_; - result.speed_ = speed_; - result.provider_ = provider_; - onBuilt(); - return result; - } - - public Builder clone() { - return (Builder) super.clone(); - } - public Builder setField( - com.google.protobuf.Descriptors.FieldDescriptor field, - Object value) { - return (Builder) super.setField(field, value); - } - public Builder clearField( - com.google.protobuf.Descriptors.FieldDescriptor field) { - return (Builder) super.clearField(field); - } - public Builder clearOneof( - com.google.protobuf.Descriptors.OneofDescriptor oneof) { - return (Builder) super.clearOneof(oneof); - } - public Builder setRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - int index, Object value) { - return (Builder) super.setRepeatedField(field, index, value); - } - public Builder addRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - Object value) { - return (Builder) super.addRepeatedField(field, value); - } - public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof GNSS_Sample) { - return mergeFrom((GNSS_Sample)other); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom(GNSS_Sample other) { - if (other == GNSS_Sample.getDefaultInstance()) return this; - if (other.getRelativeTimestamp() != 0L) { - setRelativeTimestamp(other.getRelativeTimestamp()); - } - if (other.getLatitude() != 0F) { - setLatitude(other.getLatitude()); - } - if (other.getLongitude() != 0F) { - setLongitude(other.getLongitude()); - } - if (other.getAltitude() != 0F) { - setAltitude(other.getAltitude()); - } - if (other.getAccuracy() != 0F) { - setAccuracy(other.getAccuracy()); - } - if (other.getSpeed() != 0F) { - setSpeed(other.getSpeed()); - } - if (!other.getProvider().isEmpty()) { - provider_ = other.provider_; - onChanged(); - } - onChanged(); - return this; - } - - public final boolean isInitialized() { - return true; - } - - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - GNSS_Sample parsedMessage = null; - try { - parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - parsedMessage = (GNSS_Sample) e.getUnfinishedMessage(); - throw e.unwrapIOException(); - } finally { - if (parsedMessage != null) { - mergeFrom(parsedMessage); - } - } - return this; - } - - private long relativeTimestamp_ ; - /** - * optional int64 relative_timestamp = 1; - */ - public long getRelativeTimestamp() { - return relativeTimestamp_; - } - /** - * optional int64 relative_timestamp = 1; - */ - public Builder setRelativeTimestamp(long value) { - - relativeTimestamp_ = value; - onChanged(); - return this; - } - /** - * optional int64 relative_timestamp = 1; - */ - public Builder clearRelativeTimestamp() { - - relativeTimestamp_ = 0L; - onChanged(); - return this; - } - - private float latitude_ ; - /** - *
-       * degrees (minimum 6 significant figures)
-       * latitude between -90 and 90
-       * 
- * - * optional float latitude = 2; - */ - public float getLatitude() { - return latitude_; - } - /** - *
-       * degrees (minimum 6 significant figures)
-       * latitude between -90 and 90
-       * 
- * - * optional float latitude = 2; - */ - public Builder setLatitude(float value) { - - latitude_ = value; - onChanged(); - return this; - } - /** - *
-       * degrees (minimum 6 significant figures)
-       * latitude between -90 and 90
-       * 
- * - * optional float latitude = 2; - */ - public Builder clearLatitude() { - - latitude_ = 0F; - onChanged(); - return this; - } - - private float longitude_ ; - /** - *
-       * longitude between -180 and 180
-       * 
- * - * optional float longitude = 3; - */ - public float getLongitude() { - return longitude_; - } - /** - *
-       * longitude between -180 and 180
-       * 
- * - * optional float longitude = 3; - */ - public Builder setLongitude(float value) { - - longitude_ = value; - onChanged(); - return this; - } - /** - *
-       * longitude between -180 and 180
-       * 
- * - * optional float longitude = 3; - */ - public Builder clearLongitude() { - - longitude_ = 0F; - onChanged(); - return this; - } - - private float altitude_ ; - /** - *
-       *metres
-       * 
- * - * optional float altitude = 4; - */ - public float getAltitude() { - return altitude_; - } - /** - *
-       *metres
-       * 
- * - * optional float altitude = 4; - */ - public Builder setAltitude(float value) { - - altitude_ = value; - onChanged(); - return this; - } - /** - *
-       *metres
-       * 
- * - * optional float altitude = 4; - */ - public Builder clearAltitude() { - - altitude_ = 0F; - onChanged(); - return this; - } - - private float accuracy_ ; - /** - *
-       * metres
-       * 
- * - * optional float accuracy = 5; - */ - public float getAccuracy() { - return accuracy_; - } - /** - *
-       * metres
-       * 
- * - * optional float accuracy = 5; - */ - public Builder setAccuracy(float value) { - - accuracy_ = value; - onChanged(); - return this; - } - /** - *
-       * metres
-       * 
- * - * optional float accuracy = 5; - */ - public Builder clearAccuracy() { - - accuracy_ = 0F; - onChanged(); - return this; - } - - private float speed_ ; - /** - *
-       * m/s
-       * 
- * - * optional float speed = 6; - */ - public float getSpeed() { - return speed_; - } - /** - *
-       * m/s
-       * 
- * - * optional float speed = 6; - */ - public Builder setSpeed(float value) { - - speed_ = value; - onChanged(); - return this; - } - /** - *
-       * m/s
-       * 
- * - * optional float speed = 6; - */ - public Builder clearSpeed() { - - speed_ = 0F; - onChanged(); - return this; - } - - private Object provider_ = ""; - /** - *
-       * e.g 'gps' or 'network'
-       * 
- * - * optional string provider = 7; - */ - public String getProvider() { - Object ref = provider_; - if (!(ref instanceof String)) { - com.google.protobuf.ByteString bs = - (com.google.protobuf.ByteString) ref; - String s = bs.toStringUtf8(); - provider_ = s; - return s; - } else { - return (String) ref; - } - } - /** - *
-       * e.g 'gps' or 'network'
-       * 
- * - * optional string provider = 7; - */ - public com.google.protobuf.ByteString - getProviderBytes() { - Object ref = provider_; - if (ref instanceof String) { - com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8( - (String) ref); - provider_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - /** - *
-       * e.g 'gps' or 'network'
-       * 
- * - * optional string provider = 7; - */ - public Builder setProvider( - String value) { - if (value == null) { - throw new NullPointerException(); - } - - provider_ = value; - onChanged(); - return this; - } - /** - *
-       * e.g 'gps' or 'network'
-       * 
- * - * optional string provider = 7; - */ - public Builder clearProvider() { - - provider_ = getDefaultInstance().getProvider(); - onChanged(); - return this; - } - /** - *
-       * e.g 'gps' or 'network'
-       * 
- * - * optional string provider = 7; - */ - public Builder setProviderBytes( - com.google.protobuf.ByteString value) { - if (value == null) { - throw new NullPointerException(); - } - checkByteStringIsUtf8(value); - - provider_ = value; - onChanged(); - return this; - } - public final Builder setUnknownFields( - final com.google.protobuf.UnknownFieldSet unknownFields) { - return this; - } - - public final Builder mergeUnknownFields( - final com.google.protobuf.UnknownFieldSet unknownFields) { - return this; - } - - - // @@protoc_insertion_point(builder_scope:GNSS_Sample) - } - - // @@protoc_insertion_point(class_scope:GNSS_Sample) - private static final GNSS_Sample DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new GNSS_Sample(); - } - - public static GNSS_Sample getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser - PARSER = new com.google.protobuf.AbstractParser() { - public GNSS_Sample parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return new GNSS_Sample(input, extensionRegistry); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - public GNSS_Sample getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - - } - - public interface WiFi_SampleOrBuilder extends - // @@protoc_insertion_point(interface_extends:WiFi_Sample) - com.google.protobuf.MessageOrBuilder { - - /** - * optional int64 relative_timestamp = 1; - */ - long getRelativeTimestamp(); - - /** - * repeated .Mac_Scan mac_scans = 2; - */ - java.util.List - getMacScansList(); - /** - * repeated .Mac_Scan mac_scans = 2; - */ - Mac_Scan getMacScans(int index); - /** - * repeated .Mac_Scan mac_scans = 2; - */ - int getMacScansCount(); - /** - * repeated .Mac_Scan mac_scans = 2; - */ - java.util.List - getMacScansOrBuilderList(); - /** - * repeated .Mac_Scan mac_scans = 2; - */ - Mac_ScanOrBuilder getMacScansOrBuilder( - int index); - } - /** - * Protobuf type {@code WiFi_Sample} - */ - public static final class WiFi_Sample extends - com.google.protobuf.GeneratedMessageV3 implements - // @@protoc_insertion_point(message_implements:WiFi_Sample) - WiFi_SampleOrBuilder { - // Use WiFi_Sample.newBuilder() to construct. - private WiFi_Sample(com.google.protobuf.GeneratedMessageV3.Builder builder) { - super(builder); - } - private WiFi_Sample() { - relativeTimestamp_ = 0L; - macScans_ = java.util.Collections.emptyList(); - } - - @Override - public final com.google.protobuf.UnknownFieldSet - getUnknownFields() { - return com.google.protobuf.UnknownFieldSet.getDefaultInstance(); - } - private WiFi_Sample( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - this(); - int mutable_bitField0_ = 0; - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - default: { - if (!input.skipField(tag)) { - done = true; - } - break; - } - case 8: { - - relativeTimestamp_ = input.readInt64(); - break; - } - case 18: { - if (!((mutable_bitField0_ & 0x00000002) == 0x00000002)) { - macScans_ = new java.util.ArrayList(); - mutable_bitField0_ |= 0x00000002; - } - macScans_.add( - input.readMessage(Mac_Scan.parser(), extensionRegistry)); - break; - } - } - } - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(this); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException( - e).setUnfinishedMessage(this); - } finally { - if (((mutable_bitField0_ & 0x00000002) == 0x00000002)) { - macScans_ = java.util.Collections.unmodifiableList(macScans_); - } - makeExtensionsImmutable(); - } - } - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return Traj.internal_static_WiFi_Sample_descriptor; - } - - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return Traj.internal_static_WiFi_Sample_fieldAccessorTable - .ensureFieldAccessorsInitialized( - WiFi_Sample.class, Builder.class); - } - - private int bitField0_; - public static final int RELATIVE_TIMESTAMP_FIELD_NUMBER = 1; - private long relativeTimestamp_; - /** - * optional int64 relative_timestamp = 1; - */ - public long getRelativeTimestamp() { - return relativeTimestamp_; - } - - public static final int MAC_SCANS_FIELD_NUMBER = 2; - private java.util.List macScans_; - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public java.util.List getMacScansList() { - return macScans_; - } - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public java.util.List - getMacScansOrBuilderList() { - return macScans_; - } - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public int getMacScansCount() { - return macScans_.size(); - } - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public Mac_Scan getMacScans(int index) { - return macScans_.get(index); - } - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public Mac_ScanOrBuilder getMacScansOrBuilder( - int index) { - return macScans_.get(index); - } - - private byte memoizedIsInitialized = -1; - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) return true; - if (isInitialized == 0) return false; - - memoizedIsInitialized = 1; - return true; - } - - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - if (relativeTimestamp_ != 0L) { - output.writeInt64(1, relativeTimestamp_); - } - for (int i = 0; i < macScans_.size(); i++) { - output.writeMessage(2, macScans_.get(i)); - } - } - - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) return size; - - size = 0; - if (relativeTimestamp_ != 0L) { - size += com.google.protobuf.CodedOutputStream - .computeInt64Size(1, relativeTimestamp_); - } - for (int i = 0; i < macScans_.size(); i++) { - size += com.google.protobuf.CodedOutputStream - .computeMessageSize(2, macScans_.get(i)); - } - memoizedSize = size; - return size; - } - - private static final long serialVersionUID = 0L; - @Override - public boolean equals(final Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof WiFi_Sample)) { - return super.equals(obj); - } - WiFi_Sample other = (WiFi_Sample) obj; - - boolean result = true; - result = result && (getRelativeTimestamp() - == other.getRelativeTimestamp()); - result = result && getMacScansList() - .equals(other.getMacScansList()); - return result; - } - - @Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptorForType().hashCode(); - hash = (37 * hash) + RELATIVE_TIMESTAMP_FIELD_NUMBER; - hash = (53 * hash) + com.google.protobuf.Internal.hashLong( - getRelativeTimestamp()); - if (getMacScansCount() > 0) { - hash = (37 * hash) + MAC_SCANS_FIELD_NUMBER; - hash = (53 * hash) + getMacScansList().hashCode(); - } - hash = (29 * hash) + unknownFields.hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static WiFi_Sample parseFrom( - com.google.protobuf.ByteString data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static WiFi_Sample parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static WiFi_Sample parseFrom(byte[] data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static WiFi_Sample parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static WiFi_Sample parseFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input); - } - public static WiFi_Sample parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input, extensionRegistry); - } - public static WiFi_Sample parseDelimitedFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseDelimitedWithIOException(PARSER, input); - } - public static WiFi_Sample parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - public static WiFi_Sample parseFrom( - com.google.protobuf.CodedInputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input); - } - public static WiFi_Sample parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public Builder newBuilderForType() { return newBuilder(); } - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - public static Builder newBuilder(WiFi_Sample prototype) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() : new Builder().mergeFrom(this); - } - - @Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { - Builder builder = new Builder(parent); - return builder; - } - /** - * Protobuf type {@code WiFi_Sample} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessageV3.Builder implements - // @@protoc_insertion_point(builder_implements:WiFi_Sample) - WiFi_SampleOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return Traj.internal_static_WiFi_Sample_descriptor; - } - - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return Traj.internal_static_WiFi_Sample_fieldAccessorTable - .ensureFieldAccessorsInitialized( - WiFi_Sample.class, Builder.class); - } - - // Construct using Traj.WiFi_Sample.newBuilder() - private Builder() { - maybeForceBuilderInitialization(); - } - - private Builder( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { - super(parent); - maybeForceBuilderInitialization(); - } - private void maybeForceBuilderInitialization() { - if (com.google.protobuf.GeneratedMessageV3 - .alwaysUseFieldBuilders) { - getMacScansFieldBuilder(); - } - } - public Builder clear() { - super.clear(); - relativeTimestamp_ = 0L; - - if (macScansBuilder_ == null) { - macScans_ = java.util.Collections.emptyList(); - bitField0_ = (bitField0_ & ~0x00000002); - } else { - macScansBuilder_.clear(); - } - return this; - } - - public com.google.protobuf.Descriptors.Descriptor - getDescriptorForType() { - return Traj.internal_static_WiFi_Sample_descriptor; - } - - public WiFi_Sample getDefaultInstanceForType() { - return WiFi_Sample.getDefaultInstance(); - } - - public WiFi_Sample build() { - WiFi_Sample result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - public WiFi_Sample buildPartial() { - WiFi_Sample result = new WiFi_Sample(this); - int from_bitField0_ = bitField0_; - int to_bitField0_ = 0; - result.relativeTimestamp_ = relativeTimestamp_; - if (macScansBuilder_ == null) { - if (((bitField0_ & 0x00000002) == 0x00000002)) { - macScans_ = java.util.Collections.unmodifiableList(macScans_); - bitField0_ = (bitField0_ & ~0x00000002); - } - result.macScans_ = macScans_; - } else { - result.macScans_ = macScansBuilder_.build(); - } - result.bitField0_ = to_bitField0_; - onBuilt(); - return result; - } - - public Builder clone() { - return (Builder) super.clone(); - } - public Builder setField( - com.google.protobuf.Descriptors.FieldDescriptor field, - Object value) { - return (Builder) super.setField(field, value); - } - public Builder clearField( - com.google.protobuf.Descriptors.FieldDescriptor field) { - return (Builder) super.clearField(field); - } - public Builder clearOneof( - com.google.protobuf.Descriptors.OneofDescriptor oneof) { - return (Builder) super.clearOneof(oneof); - } - public Builder setRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - int index, Object value) { - return (Builder) super.setRepeatedField(field, index, value); - } - public Builder addRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - Object value) { - return (Builder) super.addRepeatedField(field, value); - } - public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof WiFi_Sample) { - return mergeFrom((WiFi_Sample)other); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom(WiFi_Sample other) { - if (other == WiFi_Sample.getDefaultInstance()) return this; - if (other.getRelativeTimestamp() != 0L) { - setRelativeTimestamp(other.getRelativeTimestamp()); - } - if (macScansBuilder_ == null) { - if (!other.macScans_.isEmpty()) { - if (macScans_.isEmpty()) { - macScans_ = other.macScans_; - bitField0_ = (bitField0_ & ~0x00000002); - } else { - ensureMacScansIsMutable(); - macScans_.addAll(other.macScans_); - } - onChanged(); - } - } else { - if (!other.macScans_.isEmpty()) { - if (macScansBuilder_.isEmpty()) { - macScansBuilder_.dispose(); - macScansBuilder_ = null; - macScans_ = other.macScans_; - bitField0_ = (bitField0_ & ~0x00000002); - macScansBuilder_ = - com.google.protobuf.GeneratedMessageV3.alwaysUseFieldBuilders ? - getMacScansFieldBuilder() : null; - } else { - macScansBuilder_.addAllMessages(other.macScans_); - } - } - } - onChanged(); - return this; - } - - public final boolean isInitialized() { - return true; - } - - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - WiFi_Sample parsedMessage = null; - try { - parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - parsedMessage = (WiFi_Sample) e.getUnfinishedMessage(); - throw e.unwrapIOException(); - } finally { - if (parsedMessage != null) { - mergeFrom(parsedMessage); - } - } - return this; - } - private int bitField0_; - - private long relativeTimestamp_ ; - /** - * optional int64 relative_timestamp = 1; - */ - public long getRelativeTimestamp() { - return relativeTimestamp_; - } - /** - * optional int64 relative_timestamp = 1; - */ - public Builder setRelativeTimestamp(long value) { - - relativeTimestamp_ = value; - onChanged(); - return this; - } - /** - * optional int64 relative_timestamp = 1; - */ - public Builder clearRelativeTimestamp() { - - relativeTimestamp_ = 0L; - onChanged(); - return this; - } - - private java.util.List macScans_ = - java.util.Collections.emptyList(); - private void ensureMacScansIsMutable() { - if (!((bitField0_ & 0x00000002) == 0x00000002)) { - macScans_ = new java.util.ArrayList(macScans_); - bitField0_ |= 0x00000002; - } - } - - private com.google.protobuf.RepeatedFieldBuilderV3< - Mac_Scan, Mac_Scan.Builder, Mac_ScanOrBuilder> macScansBuilder_; - - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public java.util.List getMacScansList() { - if (macScansBuilder_ == null) { - return java.util.Collections.unmodifiableList(macScans_); - } else { - return macScansBuilder_.getMessageList(); - } - } - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public int getMacScansCount() { - if (macScansBuilder_ == null) { - return macScans_.size(); - } else { - return macScansBuilder_.getCount(); - } - } - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public Mac_Scan getMacScans(int index) { - if (macScansBuilder_ == null) { - return macScans_.get(index); - } else { - return macScansBuilder_.getMessage(index); - } - } - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public Builder setMacScans( - int index, Mac_Scan value) { - if (macScansBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensureMacScansIsMutable(); - macScans_.set(index, value); - onChanged(); - } else { - macScansBuilder_.setMessage(index, value); - } - return this; - } - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public Builder setMacScans( - int index, Mac_Scan.Builder builderForValue) { - if (macScansBuilder_ == null) { - ensureMacScansIsMutable(); - macScans_.set(index, builderForValue.build()); - onChanged(); - } else { - macScansBuilder_.setMessage(index, builderForValue.build()); - } - return this; - } - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public Builder addMacScans(Mac_Scan value) { - if (macScansBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensureMacScansIsMutable(); - macScans_.add(value); - onChanged(); - } else { - macScansBuilder_.addMessage(value); - } - return this; - } - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public Builder addMacScans( - int index, Mac_Scan value) { - if (macScansBuilder_ == null) { - if (value == null) { - throw new NullPointerException(); - } - ensureMacScansIsMutable(); - macScans_.add(index, value); - onChanged(); - } else { - macScansBuilder_.addMessage(index, value); - } - return this; - } - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public Builder addMacScans( - Mac_Scan.Builder builderForValue) { - if (macScansBuilder_ == null) { - ensureMacScansIsMutable(); - macScans_.add(builderForValue.build()); - onChanged(); - } else { - macScansBuilder_.addMessage(builderForValue.build()); - } - return this; - } - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public Builder addMacScans( - int index, Mac_Scan.Builder builderForValue) { - if (macScansBuilder_ == null) { - ensureMacScansIsMutable(); - macScans_.add(index, builderForValue.build()); - onChanged(); - } else { - macScansBuilder_.addMessage(index, builderForValue.build()); - } - return this; - } - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public Builder addAllMacScans( - Iterable values) { - if (macScansBuilder_ == null) { - ensureMacScansIsMutable(); - com.google.protobuf.AbstractMessageLite.Builder.addAll( - values, macScans_); - onChanged(); - } else { - macScansBuilder_.addAllMessages(values); - } - return this; - } - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public Builder clearMacScans() { - if (macScansBuilder_ == null) { - macScans_ = java.util.Collections.emptyList(); - bitField0_ = (bitField0_ & ~0x00000002); - onChanged(); - } else { - macScansBuilder_.clear(); - } - return this; - } - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public Builder removeMacScans(int index) { - if (macScansBuilder_ == null) { - ensureMacScansIsMutable(); - macScans_.remove(index); - onChanged(); - } else { - macScansBuilder_.remove(index); - } - return this; - } - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public Mac_Scan.Builder getMacScansBuilder( - int index) { - return getMacScansFieldBuilder().getBuilder(index); - } - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public Mac_ScanOrBuilder getMacScansOrBuilder( - int index) { - if (macScansBuilder_ == null) { - return macScans_.get(index); } else { - return macScansBuilder_.getMessageOrBuilder(index); - } - } - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public java.util.List - getMacScansOrBuilderList() { - if (macScansBuilder_ != null) { - return macScansBuilder_.getMessageOrBuilderList(); - } else { - return java.util.Collections.unmodifiableList(macScans_); - } - } - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public Mac_Scan.Builder addMacScansBuilder() { - return getMacScansFieldBuilder().addBuilder( - Mac_Scan.getDefaultInstance()); - } - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public Mac_Scan.Builder addMacScansBuilder( - int index) { - return getMacScansFieldBuilder().addBuilder( - index, Mac_Scan.getDefaultInstance()); - } - /** - * repeated .Mac_Scan mac_scans = 2; - */ - public java.util.List - getMacScansBuilderList() { - return getMacScansFieldBuilder().getBuilderList(); - } - private com.google.protobuf.RepeatedFieldBuilderV3< - Mac_Scan, Mac_Scan.Builder, Mac_ScanOrBuilder> - getMacScansFieldBuilder() { - if (macScansBuilder_ == null) { - macScansBuilder_ = new com.google.protobuf.RepeatedFieldBuilderV3< - Mac_Scan, Mac_Scan.Builder, Mac_ScanOrBuilder>( - macScans_, - ((bitField0_ & 0x00000002) == 0x00000002), - getParentForChildren(), - isClean()); - macScans_ = null; - } - return macScansBuilder_; - } - public final Builder setUnknownFields( - final com.google.protobuf.UnknownFieldSet unknownFields) { - return this; - } - - public final Builder mergeUnknownFields( - final com.google.protobuf.UnknownFieldSet unknownFields) { - return this; - } - - - // @@protoc_insertion_point(builder_scope:WiFi_Sample) - } - - // @@protoc_insertion_point(class_scope:WiFi_Sample) - private static final WiFi_Sample DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new WiFi_Sample(); - } - - public static WiFi_Sample getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser - PARSER = new com.google.protobuf.AbstractParser() { - public WiFi_Sample parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return new WiFi_Sample(input, extensionRegistry); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - public WiFi_Sample getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - - } - - public interface Mac_ScanOrBuilder extends - // @@protoc_insertion_point(interface_extends:Mac_Scan) - com.google.protobuf.MessageOrBuilder { - - /** - * optional int64 relative_timestamp = 1; - */ - long getRelativeTimestamp(); - - /** - *
-     * Integer encoding of the hex mac address
-     * e.g. 207394925843984
-     * 
- * - * optional int64 mac = 2; - */ - long getMac(); - - /** - *
-     * rssi integer in dBm.
-     * typically between -120 and -10
-     * 
- * - * optional int32 rssi = 3; - */ - int getRssi(); - } - /** - * Protobuf type {@code Mac_Scan} - */ - public static final class Mac_Scan extends - com.google.protobuf.GeneratedMessageV3 implements - // @@protoc_insertion_point(message_implements:Mac_Scan) - Mac_ScanOrBuilder { - // Use Mac_Scan.newBuilder() to construct. - private Mac_Scan(com.google.protobuf.GeneratedMessageV3.Builder builder) { - super(builder); - } - private Mac_Scan() { - relativeTimestamp_ = 0L; - mac_ = 0L; - rssi_ = 0; - } - - @Override - public final com.google.protobuf.UnknownFieldSet - getUnknownFields() { - return com.google.protobuf.UnknownFieldSet.getDefaultInstance(); - } - private Mac_Scan( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - this(); - int mutable_bitField0_ = 0; - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - default: { - if (!input.skipField(tag)) { - done = true; - } - break; - } - case 8: { - - relativeTimestamp_ = input.readInt64(); - break; - } - case 16: { - - mac_ = input.readInt64(); - break; - } - case 24: { - - rssi_ = input.readInt32(); - break; - } - } - } - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(this); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException( - e).setUnfinishedMessage(this); - } finally { - makeExtensionsImmutable(); - } - } - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return Traj.internal_static_Mac_Scan_descriptor; - } - - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return Traj.internal_static_Mac_Scan_fieldAccessorTable - .ensureFieldAccessorsInitialized( - Mac_Scan.class, Builder.class); - } - - public static final int RELATIVE_TIMESTAMP_FIELD_NUMBER = 1; - private long relativeTimestamp_; - /** - * optional int64 relative_timestamp = 1; - */ - public long getRelativeTimestamp() { - return relativeTimestamp_; - } - - public static final int MAC_FIELD_NUMBER = 2; - private long mac_; - /** - *
-     * Integer encoding of the hex mac address
-     * e.g. 207394925843984
-     * 
- * - * optional int64 mac = 2; - */ - public long getMac() { - return mac_; - } - - public static final int RSSI_FIELD_NUMBER = 3; - private int rssi_; - /** - *
-     * rssi integer in dBm.
-     * typically between -120 and -10
-     * 
- * - * optional int32 rssi = 3; - */ - public int getRssi() { - return rssi_; - } - - private byte memoizedIsInitialized = -1; - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) return true; - if (isInitialized == 0) return false; - - memoizedIsInitialized = 1; - return true; - } - - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - if (relativeTimestamp_ != 0L) { - output.writeInt64(1, relativeTimestamp_); - } - if (mac_ != 0L) { - output.writeInt64(2, mac_); - } - if (rssi_ != 0) { - output.writeInt32(3, rssi_); - } - } - - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) return size; - - size = 0; - if (relativeTimestamp_ != 0L) { - size += com.google.protobuf.CodedOutputStream - .computeInt64Size(1, relativeTimestamp_); - } - if (mac_ != 0L) { - size += com.google.protobuf.CodedOutputStream - .computeInt64Size(2, mac_); - } - if (rssi_ != 0) { - size += com.google.protobuf.CodedOutputStream - .computeInt32Size(3, rssi_); - } - memoizedSize = size; - return size; - } - - private static final long serialVersionUID = 0L; - @Override - public boolean equals(final Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof Mac_Scan)) { - return super.equals(obj); - } - Mac_Scan other = (Mac_Scan) obj; - - boolean result = true; - result = result && (getRelativeTimestamp() - == other.getRelativeTimestamp()); - result = result && (getMac() - == other.getMac()); - result = result && (getRssi() - == other.getRssi()); - return result; - } - - @Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptorForType().hashCode(); - hash = (37 * hash) + RELATIVE_TIMESTAMP_FIELD_NUMBER; - hash = (53 * hash) + com.google.protobuf.Internal.hashLong( - getRelativeTimestamp()); - hash = (37 * hash) + MAC_FIELD_NUMBER; - hash = (53 * hash) + com.google.protobuf.Internal.hashLong( - getMac()); - hash = (37 * hash) + RSSI_FIELD_NUMBER; - hash = (53 * hash) + getRssi(); - hash = (29 * hash) + unknownFields.hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static Mac_Scan parseFrom( - com.google.protobuf.ByteString data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static Mac_Scan parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static Mac_Scan parseFrom(byte[] data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static Mac_Scan parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static Mac_Scan parseFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input); - } - public static Mac_Scan parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input, extensionRegistry); - } - public static Mac_Scan parseDelimitedFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseDelimitedWithIOException(PARSER, input); - } - public static Mac_Scan parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - public static Mac_Scan parseFrom( - com.google.protobuf.CodedInputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input); - } - public static Mac_Scan parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public Builder newBuilderForType() { return newBuilder(); } - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - public static Builder newBuilder(Mac_Scan prototype) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() : new Builder().mergeFrom(this); - } - - @Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { - Builder builder = new Builder(parent); - return builder; - } - /** - * Protobuf type {@code Mac_Scan} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessageV3.Builder implements - // @@protoc_insertion_point(builder_implements:Mac_Scan) - Mac_ScanOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return Traj.internal_static_Mac_Scan_descriptor; - } - - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return Traj.internal_static_Mac_Scan_fieldAccessorTable - .ensureFieldAccessorsInitialized( - Mac_Scan.class, Builder.class); - } - - // Construct using Traj.Mac_Scan.newBuilder() - private Builder() { - maybeForceBuilderInitialization(); - } - - private Builder( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { - super(parent); - maybeForceBuilderInitialization(); - } - private void maybeForceBuilderInitialization() { - if (com.google.protobuf.GeneratedMessageV3 - .alwaysUseFieldBuilders) { - } - } - public Builder clear() { - super.clear(); - relativeTimestamp_ = 0L; - - mac_ = 0L; - - rssi_ = 0; - - return this; - } - - public com.google.protobuf.Descriptors.Descriptor - getDescriptorForType() { - return Traj.internal_static_Mac_Scan_descriptor; - } - - public Mac_Scan getDefaultInstanceForType() { - return Mac_Scan.getDefaultInstance(); - } - - public Mac_Scan build() { - Mac_Scan result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - public Mac_Scan buildPartial() { - Mac_Scan result = new Mac_Scan(this); - result.relativeTimestamp_ = relativeTimestamp_; - result.mac_ = mac_; - result.rssi_ = rssi_; - onBuilt(); - return result; - } - - public Builder clone() { - return (Builder) super.clone(); - } - public Builder setField( - com.google.protobuf.Descriptors.FieldDescriptor field, - Object value) { - return (Builder) super.setField(field, value); - } - public Builder clearField( - com.google.protobuf.Descriptors.FieldDescriptor field) { - return (Builder) super.clearField(field); - } - public Builder clearOneof( - com.google.protobuf.Descriptors.OneofDescriptor oneof) { - return (Builder) super.clearOneof(oneof); - } - public Builder setRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - int index, Object value) { - return (Builder) super.setRepeatedField(field, index, value); - } - public Builder addRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - Object value) { - return (Builder) super.addRepeatedField(field, value); - } - public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof Mac_Scan) { - return mergeFrom((Mac_Scan)other); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom(Mac_Scan other) { - if (other == Mac_Scan.getDefaultInstance()) return this; - if (other.getRelativeTimestamp() != 0L) { - setRelativeTimestamp(other.getRelativeTimestamp()); - } - if (other.getMac() != 0L) { - setMac(other.getMac()); - } - if (other.getRssi() != 0) { - setRssi(other.getRssi()); - } - onChanged(); - return this; - } - - public final boolean isInitialized() { - return true; - } - - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - Mac_Scan parsedMessage = null; - try { - parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - parsedMessage = (Mac_Scan) e.getUnfinishedMessage(); - throw e.unwrapIOException(); - } finally { - if (parsedMessage != null) { - mergeFrom(parsedMessage); - } - } - return this; - } - - private long relativeTimestamp_ ; - /** - * optional int64 relative_timestamp = 1; - */ - public long getRelativeTimestamp() { - return relativeTimestamp_; - } - /** - * optional int64 relative_timestamp = 1; - */ - public Builder setRelativeTimestamp(long value) { - - relativeTimestamp_ = value; - onChanged(); - return this; - } - /** - * optional int64 relative_timestamp = 1; - */ - public Builder clearRelativeTimestamp() { - - relativeTimestamp_ = 0L; - onChanged(); - return this; - } - - private long mac_ ; - /** - *
-       * Integer encoding of the hex mac address
-       * e.g. 207394925843984
-       * 
- * - * optional int64 mac = 2; - */ - public long getMac() { - return mac_; - } - /** - *
-       * Integer encoding of the hex mac address
-       * e.g. 207394925843984
-       * 
- * - * optional int64 mac = 2; - */ - public Builder setMac(long value) { - - mac_ = value; - onChanged(); - return this; - } - /** - *
-       * Integer encoding of the hex mac address
-       * e.g. 207394925843984
-       * 
- * - * optional int64 mac = 2; - */ - public Builder clearMac() { - - mac_ = 0L; - onChanged(); - return this; - } - - private int rssi_ ; - /** - *
-       * rssi integer in dBm.
-       * typically between -120 and -10
-       * 
- * - * optional int32 rssi = 3; - */ - public int getRssi() { - return rssi_; - } - /** - *
-       * rssi integer in dBm.
-       * typically between -120 and -10
-       * 
- * - * optional int32 rssi = 3; - */ - public Builder setRssi(int value) { - - rssi_ = value; - onChanged(); - return this; - } - /** - *
-       * rssi integer in dBm.
-       * typically between -120 and -10
-       * 
- * - * optional int32 rssi = 3; - */ - public Builder clearRssi() { - - rssi_ = 0; - onChanged(); - return this; - } - public final Builder setUnknownFields( - final com.google.protobuf.UnknownFieldSet unknownFields) { - return this; - } - - public final Builder mergeUnknownFields( - final com.google.protobuf.UnknownFieldSet unknownFields) { - return this; - } - - - // @@protoc_insertion_point(builder_scope:Mac_Scan) - } - - // @@protoc_insertion_point(class_scope:Mac_Scan) - private static final Mac_Scan DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new Mac_Scan(); - } - - public static Mac_Scan getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser - PARSER = new com.google.protobuf.AbstractParser() { - public Mac_Scan parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return new Mac_Scan(input, extensionRegistry); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - public Mac_Scan getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - - } - - public interface AP_DataOrBuilder extends - // @@protoc_insertion_point(interface_extends:AP_Data) - com.google.protobuf.MessageOrBuilder { - - /** - *
-     * Integer encoding of the hex mac address
-     * e.g. 207394925843984
-     * 
- * - * optional int64 mac = 1; - */ - long getMac(); - - /** - *
-     * E.g. 'Eduroam' or 'Starbucks_free_wifi'
-     * 
- * - * optional string ssid = 2; - */ - String getSsid(); - /** - *
-     * E.g. 'Eduroam' or 'Starbucks_free_wifi'
-     * 
- * - * optional string ssid = 2; - */ - com.google.protobuf.ByteString - getSsidBytes(); - - /** - *
-     * Typically 2.4GHz or 5GHz
-     * 
- * - * optional int64 frequency = 3; - */ - long getFrequency(); - } - /** - * Protobuf type {@code AP_Data} - */ - public static final class AP_Data extends - com.google.protobuf.GeneratedMessageV3 implements - // @@protoc_insertion_point(message_implements:AP_Data) - AP_DataOrBuilder { - // Use AP_Data.newBuilder() to construct. - private AP_Data(com.google.protobuf.GeneratedMessageV3.Builder builder) { - super(builder); - } - private AP_Data() { - mac_ = 0L; - ssid_ = ""; - frequency_ = 0L; - } - - @Override - public final com.google.protobuf.UnknownFieldSet - getUnknownFields() { - return com.google.protobuf.UnknownFieldSet.getDefaultInstance(); - } - private AP_Data( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - this(); - int mutable_bitField0_ = 0; - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - default: { - if (!input.skipField(tag)) { - done = true; - } - break; - } - case 8: { - - mac_ = input.readInt64(); - break; - } - case 18: { - String s = input.readStringRequireUtf8(); - - ssid_ = s; - break; - } - case 24: { - - frequency_ = input.readInt64(); - break; - } - } - } - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(this); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException( - e).setUnfinishedMessage(this); - } finally { - makeExtensionsImmutable(); - } - } - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return Traj.internal_static_AP_Data_descriptor; - } - - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return Traj.internal_static_AP_Data_fieldAccessorTable - .ensureFieldAccessorsInitialized( - AP_Data.class, Builder.class); - } - - public static final int MAC_FIELD_NUMBER = 1; - private long mac_; - /** - *
-     * Integer encoding of the hex mac address
-     * e.g. 207394925843984
-     * 
- * - * optional int64 mac = 1; - */ - public long getMac() { - return mac_; - } - - public static final int SSID_FIELD_NUMBER = 2; - private volatile Object ssid_; - /** - *
-     * E.g. 'Eduroam' or 'Starbucks_free_wifi'
-     * 
- * - * optional string ssid = 2; - */ - public String getSsid() { - Object ref = ssid_; - if (ref instanceof String) { - return (String) ref; - } else { - com.google.protobuf.ByteString bs = - (com.google.protobuf.ByteString) ref; - String s = bs.toStringUtf8(); - ssid_ = s; - return s; - } - } - /** - *
-     * E.g. 'Eduroam' or 'Starbucks_free_wifi'
-     * 
- * - * optional string ssid = 2; - */ - public com.google.protobuf.ByteString - getSsidBytes() { - Object ref = ssid_; - if (ref instanceof String) { - com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8( - (String) ref); - ssid_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - - public static final int FREQUENCY_FIELD_NUMBER = 3; - private long frequency_; - /** - *
-     * Typically 2.4GHz or 5GHz
-     * 
- * - * optional int64 frequency = 3; - */ - public long getFrequency() { - return frequency_; - } - - private byte memoizedIsInitialized = -1; - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) return true; - if (isInitialized == 0) return false; - - memoizedIsInitialized = 1; - return true; - } - - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - if (mac_ != 0L) { - output.writeInt64(1, mac_); - } - if (!getSsidBytes().isEmpty()) { - com.google.protobuf.GeneratedMessageV3.writeString(output, 2, ssid_); - } - if (frequency_ != 0L) { - output.writeInt64(3, frequency_); - } - } - - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) return size; - - size = 0; - if (mac_ != 0L) { - size += com.google.protobuf.CodedOutputStream - .computeInt64Size(1, mac_); - } - if (!getSsidBytes().isEmpty()) { - size += com.google.protobuf.GeneratedMessageV3.computeStringSize(2, ssid_); - } - if (frequency_ != 0L) { - size += com.google.protobuf.CodedOutputStream - .computeInt64Size(3, frequency_); - } - memoizedSize = size; - return size; - } - - private static final long serialVersionUID = 0L; - @Override - public boolean equals(final Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof AP_Data)) { - return super.equals(obj); - } - AP_Data other = (AP_Data) obj; - - boolean result = true; - result = result && (getMac() - == other.getMac()); - result = result && getSsid() - .equals(other.getSsid()); - result = result && (getFrequency() - == other.getFrequency()); - return result; - } - - @Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptorForType().hashCode(); - hash = (37 * hash) + MAC_FIELD_NUMBER; - hash = (53 * hash) + com.google.protobuf.Internal.hashLong( - getMac()); - hash = (37 * hash) + SSID_FIELD_NUMBER; - hash = (53 * hash) + getSsid().hashCode(); - hash = (37 * hash) + FREQUENCY_FIELD_NUMBER; - hash = (53 * hash) + com.google.protobuf.Internal.hashLong( - getFrequency()); - hash = (29 * hash) + unknownFields.hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static AP_Data parseFrom( - com.google.protobuf.ByteString data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static AP_Data parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static AP_Data parseFrom(byte[] data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static AP_Data parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static AP_Data parseFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input); - } - public static AP_Data parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input, extensionRegistry); - } - public static AP_Data parseDelimitedFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseDelimitedWithIOException(PARSER, input); - } - public static AP_Data parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - public static AP_Data parseFrom( - com.google.protobuf.CodedInputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input); - } - public static AP_Data parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public Builder newBuilderForType() { return newBuilder(); } - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - public static Builder newBuilder(AP_Data prototype) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() : new Builder().mergeFrom(this); - } - - @Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { - Builder builder = new Builder(parent); - return builder; - } - /** - * Protobuf type {@code AP_Data} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessageV3.Builder implements - // @@protoc_insertion_point(builder_implements:AP_Data) - AP_DataOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return Traj.internal_static_AP_Data_descriptor; - } - - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return Traj.internal_static_AP_Data_fieldAccessorTable - .ensureFieldAccessorsInitialized( - AP_Data.class, Builder.class); - } - - // Construct using Traj.AP_Data.newBuilder() - private Builder() { - maybeForceBuilderInitialization(); - } - - private Builder( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { - super(parent); - maybeForceBuilderInitialization(); - } - private void maybeForceBuilderInitialization() { - if (com.google.protobuf.GeneratedMessageV3 - .alwaysUseFieldBuilders) { - } - } - public Builder clear() { - super.clear(); - mac_ = 0L; - - ssid_ = ""; - - frequency_ = 0L; - - return this; - } - - public com.google.protobuf.Descriptors.Descriptor - getDescriptorForType() { - return Traj.internal_static_AP_Data_descriptor; - } - - public AP_Data getDefaultInstanceForType() { - return AP_Data.getDefaultInstance(); - } - - public AP_Data build() { - AP_Data result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - public AP_Data buildPartial() { - AP_Data result = new AP_Data(this); - result.mac_ = mac_; - result.ssid_ = ssid_; - result.frequency_ = frequency_; - onBuilt(); - return result; - } - - public Builder clone() { - return (Builder) super.clone(); - } - public Builder setField( - com.google.protobuf.Descriptors.FieldDescriptor field, - Object value) { - return (Builder) super.setField(field, value); - } - public Builder clearField( - com.google.protobuf.Descriptors.FieldDescriptor field) { - return (Builder) super.clearField(field); - } - public Builder clearOneof( - com.google.protobuf.Descriptors.OneofDescriptor oneof) { - return (Builder) super.clearOneof(oneof); - } - public Builder setRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - int index, Object value) { - return (Builder) super.setRepeatedField(field, index, value); - } - public Builder addRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - Object value) { - return (Builder) super.addRepeatedField(field, value); - } - public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof AP_Data) { - return mergeFrom((AP_Data)other); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom(AP_Data other) { - if (other == AP_Data.getDefaultInstance()) return this; - if (other.getMac() != 0L) { - setMac(other.getMac()); - } - if (!other.getSsid().isEmpty()) { - ssid_ = other.ssid_; - onChanged(); - } - if (other.getFrequency() != 0L) { - setFrequency(other.getFrequency()); - } - onChanged(); - return this; - } - - public final boolean isInitialized() { - return true; - } - - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - AP_Data parsedMessage = null; - try { - parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - parsedMessage = (AP_Data) e.getUnfinishedMessage(); - throw e.unwrapIOException(); - } finally { - if (parsedMessage != null) { - mergeFrom(parsedMessage); - } - } - return this; - } - - private long mac_ ; - /** - *
-       * Integer encoding of the hex mac address
-       * e.g. 207394925843984
-       * 
- * - * optional int64 mac = 1; - */ - public long getMac() { - return mac_; - } - /** - *
-       * Integer encoding of the hex mac address
-       * e.g. 207394925843984
-       * 
- * - * optional int64 mac = 1; - */ - public Builder setMac(long value) { - - mac_ = value; - onChanged(); - return this; - } - /** - *
-       * Integer encoding of the hex mac address
-       * e.g. 207394925843984
-       * 
- * - * optional int64 mac = 1; - */ - public Builder clearMac() { - - mac_ = 0L; - onChanged(); - return this; - } - - private Object ssid_ = ""; - /** - *
-       * E.g. 'Eduroam' or 'Starbucks_free_wifi'
-       * 
- * - * optional string ssid = 2; - */ - public String getSsid() { - Object ref = ssid_; - if (!(ref instanceof String)) { - com.google.protobuf.ByteString bs = - (com.google.protobuf.ByteString) ref; - String s = bs.toStringUtf8(); - ssid_ = s; - return s; - } else { - return (String) ref; - } - } - /** - *
-       * E.g. 'Eduroam' or 'Starbucks_free_wifi'
-       * 
- * - * optional string ssid = 2; - */ - public com.google.protobuf.ByteString - getSsidBytes() { - Object ref = ssid_; - if (ref instanceof String) { - com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8( - (String) ref); - ssid_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - /** - *
-       * E.g. 'Eduroam' or 'Starbucks_free_wifi'
-       * 
- * - * optional string ssid = 2; - */ - public Builder setSsid( - String value) { - if (value == null) { - throw new NullPointerException(); - } - - ssid_ = value; - onChanged(); - return this; - } - /** - *
-       * E.g. 'Eduroam' or 'Starbucks_free_wifi'
-       * 
- * - * optional string ssid = 2; - */ - public Builder clearSsid() { - - ssid_ = getDefaultInstance().getSsid(); - onChanged(); - return this; - } - /** - *
-       * E.g. 'Eduroam' or 'Starbucks_free_wifi'
-       * 
- * - * optional string ssid = 2; - */ - public Builder setSsidBytes( - com.google.protobuf.ByteString value) { - if (value == null) { - throw new NullPointerException(); - } - checkByteStringIsUtf8(value); - - ssid_ = value; - onChanged(); - return this; - } - - private long frequency_ ; - /** - *
-       * Typically 2.4GHz or 5GHz
-       * 
- * - * optional int64 frequency = 3; - */ - public long getFrequency() { - return frequency_; - } - /** - *
-       * Typically 2.4GHz or 5GHz
-       * 
- * - * optional int64 frequency = 3; - */ - public Builder setFrequency(long value) { - - frequency_ = value; - onChanged(); - return this; - } - /** - *
-       * Typically 2.4GHz or 5GHz
-       * 
- * - * optional int64 frequency = 3; - */ - public Builder clearFrequency() { - - frequency_ = 0L; - onChanged(); - return this; - } - public final Builder setUnknownFields( - final com.google.protobuf.UnknownFieldSet unknownFields) { - return this; - } - - public final Builder mergeUnknownFields( - final com.google.protobuf.UnknownFieldSet unknownFields) { - return this; - } - - - // @@protoc_insertion_point(builder_scope:AP_Data) - } - - // @@protoc_insertion_point(class_scope:AP_Data) - private static final AP_Data DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new AP_Data(); - } - - public static AP_Data getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser - PARSER = new com.google.protobuf.AbstractParser() { - public AP_Data parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return new AP_Data(input, extensionRegistry); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - public AP_Data getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - - } - - public interface Sensor_InfoOrBuilder extends - // @@protoc_insertion_point(interface_extends:Sensor_Info) - com.google.protobuf.MessageOrBuilder { - - /** - * optional string name = 1; - */ - String getName(); - /** - * optional string name = 1; - */ - com.google.protobuf.ByteString - getNameBytes(); - - /** - * optional string vendor = 2; - */ - String getVendor(); - /** - * optional string vendor = 2; - */ - com.google.protobuf.ByteString - getVendorBytes(); - - /** - * optional float resolution = 3; - */ - float getResolution(); - - /** - * optional float power = 4; - */ - float getPower(); - - /** - * optional int32 version = 5; - */ - int getVersion(); - - /** - * optional int32 type = 6; - */ - int getType(); - } - /** - * Protobuf type {@code Sensor_Info} - */ - public static final class Sensor_Info extends - com.google.protobuf.GeneratedMessageV3 implements - // @@protoc_insertion_point(message_implements:Sensor_Info) - Sensor_InfoOrBuilder { - // Use Sensor_Info.newBuilder() to construct. - private Sensor_Info(com.google.protobuf.GeneratedMessageV3.Builder builder) { - super(builder); - } - private Sensor_Info() { - name_ = ""; - vendor_ = ""; - resolution_ = 0F; - power_ = 0F; - version_ = 0; - type_ = 0; - } - - @Override - public final com.google.protobuf.UnknownFieldSet - getUnknownFields() { - return com.google.protobuf.UnknownFieldSet.getDefaultInstance(); - } - private Sensor_Info( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - this(); - int mutable_bitField0_ = 0; - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - default: { - if (!input.skipField(tag)) { - done = true; - } - break; - } - case 10: { - String s = input.readStringRequireUtf8(); - - name_ = s; - break; - } - case 18: { - String s = input.readStringRequireUtf8(); - - vendor_ = s; - break; - } - case 29: { - - resolution_ = input.readFloat(); - break; - } - case 37: { - - power_ = input.readFloat(); - break; - } - case 40: { - - version_ = input.readInt32(); - break; - } - case 48: { - - type_ = input.readInt32(); - break; - } - } - } - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(this); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException( - e).setUnfinishedMessage(this); - } finally { - makeExtensionsImmutable(); - } - } - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return Traj.internal_static_Sensor_Info_descriptor; - } - - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return Traj.internal_static_Sensor_Info_fieldAccessorTable - .ensureFieldAccessorsInitialized( - Sensor_Info.class, Builder.class); - } - - public static final int NAME_FIELD_NUMBER = 1; - private volatile Object name_; - /** - * optional string name = 1; - */ - public String getName() { - Object ref = name_; - if (ref instanceof String) { - return (String) ref; - } else { - com.google.protobuf.ByteString bs = - (com.google.protobuf.ByteString) ref; - String s = bs.toStringUtf8(); - name_ = s; - return s; - } - } - /** - * optional string name = 1; - */ - public com.google.protobuf.ByteString - getNameBytes() { - Object ref = name_; - if (ref instanceof String) { - com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8( - (String) ref); - name_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - - public static final int VENDOR_FIELD_NUMBER = 2; - private volatile Object vendor_; - /** - * optional string vendor = 2; - */ - public String getVendor() { - Object ref = vendor_; - if (ref instanceof String) { - return (String) ref; - } else { - com.google.protobuf.ByteString bs = - (com.google.protobuf.ByteString) ref; - String s = bs.toStringUtf8(); - vendor_ = s; - return s; - } - } - /** - * optional string vendor = 2; - */ - public com.google.protobuf.ByteString - getVendorBytes() { - Object ref = vendor_; - if (ref instanceof String) { - com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8( - (String) ref); - vendor_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - - public static final int RESOLUTION_FIELD_NUMBER = 3; - private float resolution_; - /** - * optional float resolution = 3; - */ - public float getResolution() { - return resolution_; - } - - public static final int POWER_FIELD_NUMBER = 4; - private float power_; - /** - * optional float power = 4; - */ - public float getPower() { - return power_; - } - - public static final int VERSION_FIELD_NUMBER = 5; - private int version_; - /** - * optional int32 version = 5; - */ - public int getVersion() { - return version_; - } - - public static final int TYPE_FIELD_NUMBER = 6; - private int type_; - /** - * optional int32 type = 6; - */ - public int getType() { - return type_; - } - - private byte memoizedIsInitialized = -1; - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) return true; - if (isInitialized == 0) return false; - - memoizedIsInitialized = 1; - return true; - } - - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - if (!getNameBytes().isEmpty()) { - com.google.protobuf.GeneratedMessageV3.writeString(output, 1, name_); - } - if (!getVendorBytes().isEmpty()) { - com.google.protobuf.GeneratedMessageV3.writeString(output, 2, vendor_); - } - if (resolution_ != 0F) { - output.writeFloat(3, resolution_); - } - if (power_ != 0F) { - output.writeFloat(4, power_); - } - if (version_ != 0) { - output.writeInt32(5, version_); - } - if (type_ != 0) { - output.writeInt32(6, type_); - } - } - - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) return size; - - size = 0; - if (!getNameBytes().isEmpty()) { - size += com.google.protobuf.GeneratedMessageV3.computeStringSize(1, name_); - } - if (!getVendorBytes().isEmpty()) { - size += com.google.protobuf.GeneratedMessageV3.computeStringSize(2, vendor_); - } - if (resolution_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(3, resolution_); - } - if (power_ != 0F) { - size += com.google.protobuf.CodedOutputStream - .computeFloatSize(4, power_); - } - if (version_ != 0) { - size += com.google.protobuf.CodedOutputStream - .computeInt32Size(5, version_); - } - if (type_ != 0) { - size += com.google.protobuf.CodedOutputStream - .computeInt32Size(6, type_); - } - memoizedSize = size; - return size; - } - - private static final long serialVersionUID = 0L; - @Override - public boolean equals(final Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof Sensor_Info)) { - return super.equals(obj); - } - Sensor_Info other = (Sensor_Info) obj; - - boolean result = true; - result = result && getName() - .equals(other.getName()); - result = result && getVendor() - .equals(other.getVendor()); - result = result && ( - Float.floatToIntBits(getResolution()) - == Float.floatToIntBits( - other.getResolution())); - result = result && ( - Float.floatToIntBits(getPower()) - == Float.floatToIntBits( - other.getPower())); - result = result && (getVersion() - == other.getVersion()); - result = result && (getType() - == other.getType()); - return result; - } - - @Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptorForType().hashCode(); - hash = (37 * hash) + NAME_FIELD_NUMBER; - hash = (53 * hash) + getName().hashCode(); - hash = (37 * hash) + VENDOR_FIELD_NUMBER; - hash = (53 * hash) + getVendor().hashCode(); - hash = (37 * hash) + RESOLUTION_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getResolution()); - hash = (37 * hash) + POWER_FIELD_NUMBER; - hash = (53 * hash) + Float.floatToIntBits( - getPower()); - hash = (37 * hash) + VERSION_FIELD_NUMBER; - hash = (53 * hash) + getVersion(); - hash = (37 * hash) + TYPE_FIELD_NUMBER; - hash = (53 * hash) + getType(); - hash = (29 * hash) + unknownFields.hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static Sensor_Info parseFrom( - com.google.protobuf.ByteString data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static Sensor_Info parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static Sensor_Info parseFrom(byte[] data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static Sensor_Info parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static Sensor_Info parseFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input); - } - public static Sensor_Info parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input, extensionRegistry); - } - public static Sensor_Info parseDelimitedFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseDelimitedWithIOException(PARSER, input); - } - public static Sensor_Info parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - public static Sensor_Info parseFrom( - com.google.protobuf.CodedInputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input); - } - public static Sensor_Info parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessageV3 - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public Builder newBuilderForType() { return newBuilder(); } - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - public static Builder newBuilder(Sensor_Info prototype) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() : new Builder().mergeFrom(this); - } - - @Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { - Builder builder = new Builder(parent); - return builder; - } - /** - * Protobuf type {@code Sensor_Info} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessageV3.Builder implements - // @@protoc_insertion_point(builder_implements:Sensor_Info) - Sensor_InfoOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return Traj.internal_static_Sensor_Info_descriptor; - } - - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return Traj.internal_static_Sensor_Info_fieldAccessorTable - .ensureFieldAccessorsInitialized( - Sensor_Info.class, Builder.class); - } - - // Construct using Traj.Sensor_Info.newBuilder() - private Builder() { - maybeForceBuilderInitialization(); - } - - private Builder( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { - super(parent); - maybeForceBuilderInitialization(); - } - private void maybeForceBuilderInitialization() { - if (com.google.protobuf.GeneratedMessageV3 - .alwaysUseFieldBuilders) { - } - } - public Builder clear() { - super.clear(); - name_ = ""; - - vendor_ = ""; - - resolution_ = 0F; - - power_ = 0F; - - version_ = 0; - - type_ = 0; - - return this; - } - - public com.google.protobuf.Descriptors.Descriptor - getDescriptorForType() { - return Traj.internal_static_Sensor_Info_descriptor; - } - - public Sensor_Info getDefaultInstanceForType() { - return Sensor_Info.getDefaultInstance(); - } - - public Sensor_Info build() { - Sensor_Info result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - public Sensor_Info buildPartial() { - Sensor_Info result = new Sensor_Info(this); - result.name_ = name_; - result.vendor_ = vendor_; - result.resolution_ = resolution_; - result.power_ = power_; - result.version_ = version_; - result.type_ = type_; - onBuilt(); - return result; - } - - public Builder clone() { - return (Builder) super.clone(); - } - public Builder setField( - com.google.protobuf.Descriptors.FieldDescriptor field, - Object value) { - return (Builder) super.setField(field, value); - } - public Builder clearField( - com.google.protobuf.Descriptors.FieldDescriptor field) { - return (Builder) super.clearField(field); - } - public Builder clearOneof( - com.google.protobuf.Descriptors.OneofDescriptor oneof) { - return (Builder) super.clearOneof(oneof); - } - public Builder setRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - int index, Object value) { - return (Builder) super.setRepeatedField(field, index, value); - } - public Builder addRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - Object value) { - return (Builder) super.addRepeatedField(field, value); - } - public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof Sensor_Info) { - return mergeFrom((Sensor_Info)other); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom(Sensor_Info other) { - if (other == Sensor_Info.getDefaultInstance()) return this; - if (!other.getName().isEmpty()) { - name_ = other.name_; - onChanged(); - } - if (!other.getVendor().isEmpty()) { - vendor_ = other.vendor_; - onChanged(); - } - if (other.getResolution() != 0F) { - setResolution(other.getResolution()); - } - if (other.getPower() != 0F) { - setPower(other.getPower()); - } - if (other.getVersion() != 0) { - setVersion(other.getVersion()); - } - if (other.getType() != 0) { - setType(other.getType()); - } - onChanged(); - return this; - } - - public final boolean isInitialized() { - return true; - } - - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - Sensor_Info parsedMessage = null; - try { - parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - parsedMessage = (Sensor_Info) e.getUnfinishedMessage(); - throw e.unwrapIOException(); - } finally { - if (parsedMessage != null) { - mergeFrom(parsedMessage); - } - } - return this; - } - - private Object name_ = ""; - /** - * optional string name = 1; - */ - public String getName() { - Object ref = name_; - if (!(ref instanceof String)) { - com.google.protobuf.ByteString bs = - (com.google.protobuf.ByteString) ref; - String s = bs.toStringUtf8(); - name_ = s; - return s; - } else { - return (String) ref; - } - } - /** - * optional string name = 1; - */ - public com.google.protobuf.ByteString - getNameBytes() { - Object ref = name_; - if (ref instanceof String) { - com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8( - (String) ref); - name_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - /** - * optional string name = 1; - */ - public Builder setName( - String value) { - if (value == null) { - throw new NullPointerException(); - } - - name_ = value; - onChanged(); - return this; - } - /** - * optional string name = 1; - */ - public Builder clearName() { - - name_ = getDefaultInstance().getName(); - onChanged(); - return this; - } - /** - * optional string name = 1; - */ - public Builder setNameBytes( - com.google.protobuf.ByteString value) { - if (value == null) { - throw new NullPointerException(); - } - checkByteStringIsUtf8(value); - - name_ = value; - onChanged(); - return this; - } - - private Object vendor_ = ""; - /** - * optional string vendor = 2; - */ - public String getVendor() { - Object ref = vendor_; - if (!(ref instanceof String)) { - com.google.protobuf.ByteString bs = - (com.google.protobuf.ByteString) ref; - String s = bs.toStringUtf8(); - vendor_ = s; - return s; - } else { - return (String) ref; - } - } - /** - * optional string vendor = 2; - */ - public com.google.protobuf.ByteString - getVendorBytes() { - Object ref = vendor_; - if (ref instanceof String) { - com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8( - (String) ref); - vendor_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - /** - * optional string vendor = 2; - */ - public Builder setVendor( - String value) { - if (value == null) { - throw new NullPointerException(); - } - - vendor_ = value; - onChanged(); - return this; - } - /** - * optional string vendor = 2; - */ - public Builder clearVendor() { - - vendor_ = getDefaultInstance().getVendor(); - onChanged(); - return this; - } - /** - * optional string vendor = 2; - */ - public Builder setVendorBytes( - com.google.protobuf.ByteString value) { - if (value == null) { - throw new NullPointerException(); - } - checkByteStringIsUtf8(value); - - vendor_ = value; - onChanged(); - return this; - } - - private float resolution_ ; - /** - * optional float resolution = 3; - */ - public float getResolution() { - return resolution_; - } - /** - * optional float resolution = 3; - */ - public Builder setResolution(float value) { - - resolution_ = value; - onChanged(); - return this; - } - /** - * optional float resolution = 3; - */ - public Builder clearResolution() { - - resolution_ = 0F; - onChanged(); - return this; - } - - private float power_ ; - /** - * optional float power = 4; - */ - public float getPower() { - return power_; - } - /** - * optional float power = 4; - */ - public Builder setPower(float value) { - - power_ = value; - onChanged(); - return this; - } - /** - * optional float power = 4; - */ - public Builder clearPower() { - - power_ = 0F; - onChanged(); - return this; - } - - private int version_ ; - /** - * optional int32 version = 5; - */ - public int getVersion() { - return version_; - } - /** - * optional int32 version = 5; - */ - public Builder setVersion(int value) { - - version_ = value; - onChanged(); - return this; - } - /** - * optional int32 version = 5; - */ - public Builder clearVersion() { - - version_ = 0; - onChanged(); - return this; - } - - private int type_ ; - /** - * optional int32 type = 6; - */ - public int getType() { - return type_; - } - /** - * optional int32 type = 6; - */ - public Builder setType(int value) { - - type_ = value; - onChanged(); - return this; - } - /** - * optional int32 type = 6; - */ - public Builder clearType() { - - type_ = 0; - onChanged(); - return this; - } - public final Builder setUnknownFields( - final com.google.protobuf.UnknownFieldSet unknownFields) { - return this; - } - - public final Builder mergeUnknownFields( - final com.google.protobuf.UnknownFieldSet unknownFields) { - return this; - } - - - // @@protoc_insertion_point(builder_scope:Sensor_Info) - } - - // @@protoc_insertion_point(class_scope:Sensor_Info) - private static final Sensor_Info DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new Sensor_Info(); - } - - public static Sensor_Info getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser - PARSER = new com.google.protobuf.AbstractParser() { - public Sensor_Info parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return new Sensor_Info(input, extensionRegistry); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - public Sensor_Info getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - - } - - private static final com.google.protobuf.Descriptors.Descriptor - internal_static_Trajectory_descriptor; - private static final - com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internal_static_Trajectory_fieldAccessorTable; - private static final com.google.protobuf.Descriptors.Descriptor - internal_static_Pdr_Sample_descriptor; - private static final - com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internal_static_Pdr_Sample_fieldAccessorTable; - private static final com.google.protobuf.Descriptors.Descriptor - internal_static_Motion_Sample_descriptor; - private static final - com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internal_static_Motion_Sample_fieldAccessorTable; - private static final com.google.protobuf.Descriptors.Descriptor - internal_static_Position_Sample_descriptor; - private static final - com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internal_static_Position_Sample_fieldAccessorTable; - private static final com.google.protobuf.Descriptors.Descriptor - internal_static_Pressure_Sample_descriptor; - private static final - com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internal_static_Pressure_Sample_fieldAccessorTable; - private static final com.google.protobuf.Descriptors.Descriptor - internal_static_Light_Sample_descriptor; - private static final - com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internal_static_Light_Sample_fieldAccessorTable; - private static final com.google.protobuf.Descriptors.Descriptor - internal_static_GNSS_Sample_descriptor; - private static final - com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internal_static_GNSS_Sample_fieldAccessorTable; - private static final com.google.protobuf.Descriptors.Descriptor - internal_static_WiFi_Sample_descriptor; - private static final - com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internal_static_WiFi_Sample_fieldAccessorTable; - private static final com.google.protobuf.Descriptors.Descriptor - internal_static_Mac_Scan_descriptor; - private static final - com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internal_static_Mac_Scan_fieldAccessorTable; - private static final com.google.protobuf.Descriptors.Descriptor - internal_static_AP_Data_descriptor; - private static final - com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internal_static_AP_Data_fieldAccessorTable; - private static final com.google.protobuf.Descriptors.Descriptor - internal_static_Sensor_Info_descriptor; - private static final - com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internal_static_Sensor_Info_fieldAccessorTable; - - public static com.google.protobuf.Descriptors.FileDescriptor - getDescriptor() { - return descriptor; - } - private static com.google.protobuf.Descriptors.FileDescriptor - descriptor; - static { - String[] descriptorData = { - "\n#Cloud/app/src/main/proto/traj.proto\"\337\004" + - "\n\nTrajectory\022\027\n\017android_version\030\001 \001(\t\022 \n" + - "\010imu_data\030\002 \003(\0132\016.Motion_Sample\022\035\n\010pdr_d" + - "ata\030\003 \003(\0132\013.Pdr_Sample\022\'\n\rposition_data\030" + - "\004 \003(\0132\020.Position_Sample\022\'\n\rpressure_data" + - "\030\005 \003(\0132\020.Pressure_Sample\022!\n\nlight_data\030\006" + - " \003(\0132\r.Light_Sample\022\037\n\tgnss_data\030\007 \003(\0132\014" + - ".GNSS_Sample\022\037\n\twifi_data\030\010 \003(\0132\014.WiFi_S" + - "ample\022\032\n\010aps_data\030\t \003(\0132\010.AP_Data\022\027\n\017sta" + - "rt_timestamp\030\n \001(\003\022\027\n\017data_identifier\030\013 ", - "\001(\t\022(\n\022accelerometer_info\030\014 \001(\0132\014.Sensor" + - "_Info\022$\n\016gyroscope_info\030\r \001(\0132\014.Sensor_I" + - "nfo\022*\n\024rotation_vector_info\030\016 \001(\0132\014.Sens" + - "or_Info\022\'\n\021magnetometer_info\030\017 \001(\0132\014.Sen" + - "sor_Info\022$\n\016barometer_info\030\020 \001(\0132\014.Senso" + - "r_Info\022\'\n\021light_sensor_info\030\021 \001(\0132\014.Sens" + - "or_Info\">\n\nPdr_Sample\022\032\n\022relative_timest" + - "amp\030\001 \001(\003\022\t\n\001x\030\002 \001(\002\022\t\n\001y\030\003 \001(\002\"\205\002\n\rMoti" + - "on_Sample\022\032\n\022relative_timestamp\030\001 \001(\003\022\r\n" + - "\005acc_x\030\002 \001(\002\022\r\n\005acc_y\030\003 \001(\002\022\r\n\005acc_z\030\004 \001", - "(\002\022\r\n\005gyr_x\030\005 \001(\002\022\r\n\005gyr_y\030\006 \001(\002\022\r\n\005gyr_" + - "z\030\007 \001(\002\022\031\n\021rotation_vector_x\030\010 \001(\002\022\031\n\021ro" + - "tation_vector_y\030\t \001(\002\022\031\n\021rotation_vector" + - "_z\030\n \001(\002\022\031\n\021rotation_vector_w\030\013 \001(\002\022\022\n\ns" + - "tep_count\030\014 \001(\005\"Z\n\017Position_Sample\022\032\n\022re" + - "lative_timestamp\030\001 \001(\003\022\r\n\005mag_x\030\002 \001(\002\022\r\n" + - "\005mag_y\030\003 \001(\002\022\r\n\005mag_z\030\004 \001(\002\"?\n\017Pressure_" + - "Sample\022\032\n\022relative_timestamp\030\001 \001(\003\022\020\n\010pr" + - "essure\030\002 \001(\002\"9\n\014Light_Sample\022\032\n\022relative" + - "_timestamp\030\001 \001(\003\022\r\n\005light\030\002 \001(\002\"\223\001\n\013GNSS", - "_Sample\022\032\n\022relative_timestamp\030\001 \001(\003\022\020\n\010l" + - "atitude\030\002 \001(\002\022\021\n\tlongitude\030\003 \001(\002\022\020\n\010alti" + - "tude\030\004 \001(\002\022\020\n\010accuracy\030\005 \001(\002\022\r\n\005speed\030\006 " + - "\001(\002\022\020\n\010provider\030\007 \001(\t\"G\n\013WiFi_Sample\022\032\n\022" + - "relative_timestamp\030\001 \001(\003\022\034\n\tmac_scans\030\002 " + - "\003(\0132\t.Mac_Scan\"A\n\010Mac_Scan\022\032\n\022relative_t" + - "imestamp\030\001 \001(\003\022\013\n\003mac\030\002 \001(\003\022\014\n\004rssi\030\003 \001(" + - "\005\"7\n\007AP_Data\022\013\n\003mac\030\001 \001(\003\022\014\n\004ssid\030\002 \001(\t\022" + - "\021\n\tfrequency\030\003 \001(\003\"m\n\013Sensor_Info\022\014\n\004nam" + - "e\030\001 \001(\t\022\016\n\006vendor\030\002 \001(\t\022\022\n\nresolution\030\003 ", - "\001(\002\022\r\n\005power\030\004 \001(\002\022\017\n\007version\030\005 \001(\005\022\014\n\004t" + - "ype\030\006 \001(\005b\006proto3" - }; - com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = - new com.google.protobuf.Descriptors.FileDescriptor. InternalDescriptorAssigner() { - public com.google.protobuf.ExtensionRegistry assignDescriptors( - com.google.protobuf.Descriptors.FileDescriptor root) { - descriptor = root; - return null; - } - }; - com.google.protobuf.Descriptors.FileDescriptor - .internalBuildGeneratedFileFrom(descriptorData, - new com.google.protobuf.Descriptors.FileDescriptor[] { - }, assigner); - internal_static_Trajectory_descriptor = - getDescriptor().getMessageTypes().get(0); - internal_static_Trajectory_fieldAccessorTable = new - com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( - internal_static_Trajectory_descriptor, - new String[] { "AndroidVersion", "ImuData", "PdrData", "PositionData", "PressureData", "LightData", "GnssData", "WifiData", "ApsData", "StartTimestamp", "DataIdentifier", "AccelerometerInfo", "GyroscopeInfo", "RotationVectorInfo", "MagnetometerInfo", "BarometerInfo", "LightSensorInfo", }); - internal_static_Pdr_Sample_descriptor = - getDescriptor().getMessageTypes().get(1); - internal_static_Pdr_Sample_fieldAccessorTable = new - com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( - internal_static_Pdr_Sample_descriptor, - new String[] { "RelativeTimestamp", "X", "Y", }); - internal_static_Motion_Sample_descriptor = - getDescriptor().getMessageTypes().get(2); - internal_static_Motion_Sample_fieldAccessorTable = new - com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( - internal_static_Motion_Sample_descriptor, - new String[] { "RelativeTimestamp", "AccX", "AccY", "AccZ", "GyrX", "GyrY", "GyrZ", "RotationVectorX", "RotationVectorY", "RotationVectorZ", "RotationVectorW", "StepCount", }); - internal_static_Position_Sample_descriptor = - getDescriptor().getMessageTypes().get(3); - internal_static_Position_Sample_fieldAccessorTable = new - com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( - internal_static_Position_Sample_descriptor, - new String[] { "RelativeTimestamp", "MagX", "MagY", "MagZ", }); - internal_static_Pressure_Sample_descriptor = - getDescriptor().getMessageTypes().get(4); - internal_static_Pressure_Sample_fieldAccessorTable = new - com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( - internal_static_Pressure_Sample_descriptor, - new String[] { "RelativeTimestamp", "Pressure", }); - internal_static_Light_Sample_descriptor = - getDescriptor().getMessageTypes().get(5); - internal_static_Light_Sample_fieldAccessorTable = new - com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( - internal_static_Light_Sample_descriptor, - new String[] { "RelativeTimestamp", "Light", }); - internal_static_GNSS_Sample_descriptor = - getDescriptor().getMessageTypes().get(6); - internal_static_GNSS_Sample_fieldAccessorTable = new - com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( - internal_static_GNSS_Sample_descriptor, - new String[] { "RelativeTimestamp", "Latitude", "Longitude", "Altitude", "Accuracy", "Speed", "Provider", }); - internal_static_WiFi_Sample_descriptor = - getDescriptor().getMessageTypes().get(7); - internal_static_WiFi_Sample_fieldAccessorTable = new - com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( - internal_static_WiFi_Sample_descriptor, - new String[] { "RelativeTimestamp", "MacScans", }); - internal_static_Mac_Scan_descriptor = - getDescriptor().getMessageTypes().get(8); - internal_static_Mac_Scan_fieldAccessorTable = new - com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( - internal_static_Mac_Scan_descriptor, - new String[] { "RelativeTimestamp", "Mac", "Rssi", }); - internal_static_AP_Data_descriptor = - getDescriptor().getMessageTypes().get(9); - internal_static_AP_Data_fieldAccessorTable = new - com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( - internal_static_AP_Data_descriptor, - new String[] { "Mac", "Ssid", "Frequency", }); - internal_static_Sensor_Info_descriptor = - getDescriptor().getMessageTypes().get(10); - internal_static_Sensor_Info_fieldAccessorTable = new - com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( - internal_static_Sensor_Info_descriptor, - new String[] { "Name", "Vendor", "Resolution", "Power", "Version", "Type", }); - } - - // @@protoc_insertion_point(outer_class_scope) -} 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 index 2d2b1cbf..470cea52 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/data/local/TrajParser.java +++ b/app/src/main/java/com/openpositioning/PositionMe/data/local/TrajParser.java @@ -1,256 +1,371 @@ 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 com.openpositioning.PositionMe.sensors.SensorTypes; + import java.io.File; -import java.io.FileReader; +import java.io.FileInputStream; +import java.io.IOException; 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 - */ +import com.openpositioning.PositionMe.Traj; +// Parses trajectory protobuf data into structures used by replay and charts. public class TrajParser { private static final String TAG = "TrajParser"; + private static final String DIAG_TAG = "ReplayDiag"; + private Traj.Trajectory trajectory; + + public TrajParser(Traj.Trajectory trajectory) { + this.trajectory = trajectory; + } - /** - * 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; + public LatLng pdrLocation; + public float orientation; + public LatLng gnssLocation; + public LatLng wifiLocation; + + public ReplayPoint(LatLng pdr, float ori, LatLng gnss, LatLng wifi) { + this.pdrLocation = pdr; + this.orientation = ori; + this.gnssLocation = gnss; + this.wifiLocation = wifi; } } - /** Represents an IMU (Inertial Measurement Unit) data record used for orientation calculations. */ - private static class ImuRecord { - public long relativeTimestamp; - public float accX, accY, accZ; // Accelerometer values - public float gyrX, gyrY, gyrZ; // Gyroscope values - public float rotationVectorX, rotationVectorY, rotationVectorZ, rotationVectorW; // Rotation quaternion - } + private static class TimedLocation { + long timestamp; + LatLng location; - /** Represents a Pedestrian Dead Reckoning (PDR) data record storing position shifts over time. */ - private static class PdrRecord { - public long relativeTimestamp; - public float x, y; // Position relative to the starting point + TimedLocation(long timestamp, LatLng location) { + this.timestamp = timestamp; + this.location = location; + } } - /** Represents a GNSS (Global Navigation Satellite System) data record with latitude/longitude. */ - private static class GnssRecord { - public long relativeTimestamp; - public double latitude, longitude; // GNSS coordinates - } + // Builds replay points from one trajectory file. + // When PDR exists, replay follows the PDR timestamp sequence and movement path. + // GNSS and WiFi samples are aligned by time and attached as nearest historical references. + // If PDR is unavailable, replay is generated from GNSS points. + // If both timelines are missing but initialPosition is valid, a single anchor point is returned. + public static List parseTrajectoryData(String filePath, Context context, float startLat, float startLon) { + List replayPoints = new ArrayList<>(); + File file = new File(filePath); + + if (!file.exists()) { + Log.e(TAG, "File not found: " + filePath); + Log.e(DIAG_TAG, "status=FILE_BAD reason=NOT_FOUND path=" + filePath); + return replayPoints; + } - /** - * 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); + Log.e(DIAG_TAG, "status=FILE_BAD reason=NOT_READABLE path=" + filePath); + return replayPoints; + } + + try (FileInputStream fis = new FileInputStream(file)) { + Traj.Trajectory traj = Traj.Trajectory.parseFrom(fis); + List pdrList = traj.getPdrDataList(); + List gnssTimedLocations = extractGnssTimedLocations(traj); + List wifiTimedLocations = extractWifiTimedLocations(traj); + int wifiFingerprintCount = traj.getWifiFingerprintsCount(); + int wifiWithPositionCount = wifiTimedLocations.size(); + boolean hasInitialPosition = traj.hasInitialPosition(); + + Log.i(DIAG_TAG, + "status=PARSED file=" + file.getName() + + " pdr=" + pdrList.size() + + " gnss=" + gnssTimedLocations.size() + + " wifi_fp=" + wifiFingerprintCount + + " wifi_pos=" + wifiWithPositionCount + + " initial=" + hasInitialPosition); + + double originLat = startLat; + double originLon = startLon; + if (!isValidLatLon(originLat, originLon)) { + if (traj.hasInitialPosition()) { + originLat = traj.getInitialPosition().getLatitude(); + originLon = traj.getInitialPosition().getLongitude(); + } else if (!gnssTimedLocations.isEmpty()) { + originLat = gnssTimedLocations.get(0).location.latitude; + originLon = gnssTimedLocations.get(0).location.longitude; + } } - if (!file.canRead()) { - Log.e(TAG, "File is NOT readable: " + filePath); - return result; + + // Ensure origin is valid before converting PDR x/y meters to latitude/longitude. + // If no valid source exists, use (0,0) so replay generation can still proceed. + if (!isValidLatLon(originLat, originLon)) { + originLat = 0.0; + originLon = 0.0; + Log.w(TAG, "Origin coordinates missing/invalid; falling back to (0,0) for PDR replay"); } - 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); - } + Log.d(TAG, "Parsing trajectory: PDR points=" + pdrList.size() + + ", GNSS points=" + gnssTimedLocations.size() + + ", WiFi points=" + wifiTimedLocations.size() + + ", origin=" + originLat + "," + originLon); + // PDR is treated as the primary movement path when available. + // GNSS/WiFi are attached as auxiliary references for each replay point. + if (!pdrList.isEmpty()) { + Log.d(TAG, "Using PDR data as primary timeline (legacy-compatible)"); - double lat = originLat + pdr.y * 1E-5; - double lng = originLng + pdr.x * 1E-5; - LatLng pdrLocation = new LatLng(lat, lng); + double metersPerDegLat = 111132.954 - 559.822 * Math.cos(2 * originLat * Math.PI / 180); + double metersPerDegLon = 111412.84 * Math.cos(originLat * Math.PI / 180); + if (Math.abs(metersPerDegLon) < 1e-6) { + metersPerDegLon = 1e-6; + } - GnssRecord closestGnss = findClosestGnssRecord(gnssList, pdr.relativeTimestamp); - LatLng gnssLocation = closestGnss != null ? - new LatLng(closestGnss.latitude, closestGnss.longitude) : null; + int gnssIndex = 0; + int wifiIndex = 0; + LatLng matchedGnss = null; + LatLng matchedWifi = null; + float lastOrientation = 0f; + + for (int i = 0; i < pdrList.size(); i++) { + Traj.RelativePosition pdr = pdrList.get(i); + long pdrTime = pdr.getRelativeTimestamp(); + + // Convert relative PDR displacement (meters) into global coordinates. + double deltaLat = pdr.getY() / metersPerDegLat; + double deltaLon = pdr.getX() / metersPerDegLon; + LatLng currentPdrLatLng = new LatLng(originLat + deltaLat, originLon + deltaLon); + + // Estimate heading from the vector between consecutive PDR points. + float orientation = lastOrientation; + if (i > 0) { + Traj.RelativePosition prev = pdrList.get(i - 1); + double dx = pdr.getX() - prev.getX(); + double dy = pdr.getY() - prev.getY(); + if (Math.hypot(dx, dy) > 1e-4) { + orientation = (float) Math.toDegrees(Math.atan2(dx, dy)); + lastOrientation = orientation; + } + } + + // Advance GNSS pointer to the latest sample not newer than this PDR timestamp. + while (gnssIndex < gnssTimedLocations.size() + && gnssTimedLocations.get(gnssIndex).timestamp <= pdrTime) { + matchedGnss = gnssTimedLocations.get(gnssIndex).location; + gnssIndex++; + } + + // Advance WiFi pointer using the same time-alignment rule. + while (wifiIndex < wifiTimedLocations.size() + && wifiTimedLocations.get(wifiIndex).timestamp <= pdrTime) { + matchedWifi = wifiTimedLocations.get(wifiIndex).location; + wifiIndex++; + } + + replayPoints.add(new ReplayPoint(currentPdrLatLng, orientation, matchedGnss, matchedWifi)); + } + Log.d(TAG, "Created " + replayPoints.size() + " replay points from PDR timeline"); + + } else if (!gnssTimedLocations.isEmpty()) { + // If PDR is unavailable, replay directly on GNSS positions. + Log.d(TAG, "No PDR data, falling back to GNSS as primary trajectory"); + for (int i = 0; i < gnssTimedLocations.size(); i++) { + LatLng gnssLatLng = gnssTimedLocations.get(i).location; + + float orientation = 0f; + if (i > 0) { + LatLng prev = gnssTimedLocations.get(i - 1).location; + double dx = gnssLatLng.longitude - prev.longitude; + double dy = gnssLatLng.latitude - prev.latitude; + orientation = (float) Math.toDegrees(Math.atan2(dx, dy)); + } + + replayPoints.add(new ReplayPoint(gnssLatLng, orientation, gnssLatLng, null)); + } + Log.d(TAG, "Created " + replayPoints.size() + " replay points from GNSS data"); + } else if (traj.hasInitialPosition() && isValidLatLon(traj.getInitialPosition().getLatitude(), traj.getInitialPosition().getLongitude())) { + // Minimal replay output for files that only contain an initial position. + LatLng init = new LatLng(traj.getInitialPosition().getLatitude(), traj.getInitialPosition().getLongitude()); + replayPoints.add(new ReplayPoint(init, 0f, init, null)); + Log.w(TAG, "No PDR/GNSS timeline found; created single-point replay from initial position"); + } else { + Log.e(TAG, "No PDR or GNSS data found in trajectory!"); + } - result.add(new ReplayPoint(pdrLocation, gnssLocation, orientationDeg, - 0f, pdr.relativeTimestamp)); + if (replayPoints.isEmpty()) { + boolean hasReplaySourceData = !pdrList.isEmpty() || !gnssTimedLocations.isEmpty() || hasInitialPosition; + if (hasReplaySourceData) { + Log.e(DIAG_TAG, + "status=PARSER_EMPTY_OUTPUT reason=HAS_SOURCE_DATA_BUT_NO_POINTS" + + " pdr=" + pdrList.size() + + " gnss=" + gnssTimedLocations.size() + + " initial=" + hasInitialPosition); + } else { + Log.e(DIAG_TAG, + "status=FILE_NO_REPLAY_DATA reason=NO_PDR_NO_GNSS_NO_INITIAL" + + " wifi_fp=" + wifiFingerprintCount + + " wifi_pos=" + wifiWithPositionCount); + } + } else { + Log.i(DIAG_TAG, "status=OK replay_points=" + replayPoints.size()); } + } catch (IOException e) { + Log.e(TAG, "Error parsing trajectory file", e); + Log.e(DIAG_TAG, "status=FILE_BAD reason=PARSE_EXCEPTION path=" + filePath + " error=" + e.getClass().getSimpleName() + ":" + e.getMessage()); + } + return replayPoints; + } + + private static List extractGnssTimedLocations(Traj.Trajectory traj) { + List timedLocations = new ArrayList<>(); + // Keep only GNSS entries with valid coordinates for replay matching. + for (Traj.GNSSReading reading : traj.getGnssDataList()) { + if (!reading.hasPosition()) { + continue; + } + Traj.GNSSPosition pos = reading.getPosition(); + if (!isValidLatLon(pos.getLatitude(), pos.getLongitude())) { + continue; + } + timedLocations.add(new TimedLocation( + pos.getRelativeTimestamp(), + new LatLng(pos.getLatitude(), pos.getLongitude()) + )); + } + return timedLocations; + } - Collections.sort(result, Comparator.comparingLong(rp -> rp.timestamp)); + private static boolean isValidLatLon(double lat, double lon) { + if (Double.isNaN(lat) || Double.isNaN(lon)) { + return false; + } + if (lat < -90.0 || lat > 90.0 || lon < -180.0 || lon > 180.0) { + return false; + } + return !(Math.abs(lat) < 1e-8 && Math.abs(lon) < 1e-8); + } - Log.i(TAG, "Final ReplayPoints count: " + result.size()); + private static List extractWifiTimedLocations(Traj.Trajectory traj) { + List timedLocations = new ArrayList<>(); + // A WiFi fingerprint can contain multiple RF scans. + // Replay uses the first scan that includes a position payload. + for (Traj.Fingerprint fingerprint : traj.getWifiFingerprintsList()) { + LatLng bestLocation = null; + for (Traj.RFScan scan : fingerprint.getRfScansList()) { + if (scan.hasPosition()) { + Traj.GNSSPosition pos = scan.getPosition(); + bestLocation = new LatLng(pos.getLatitude(), pos.getLongitude()); + break; + } + } + if (bestLocation != null) { + timedLocations.add(new TimedLocation(fingerprint.getRelativeTimestamp(), bestLocation)); + } + } + return timedLocations; + } - } catch (Exception e) { - Log.e(TAG, "Error parsing trajectory file!", e); + // Returns true if the trajectory contains at least one GNSS reading. + public static boolean hasGnssData(String filePath) { + File file = new File(filePath); + if (!file.exists()) return false; + + try (FileInputStream fis = new FileInputStream(file)) { + Traj.Trajectory traj = Traj.Trajectory.parseFrom(fis); + return !traj.getGnssDataList().isEmpty(); + } catch (IOException e) { + Log.e(TAG, "Error parsing trajectory file for GNSS check", e); + return false; } + } - return result; + // Returns the first GNSS coordinate in the trajectory, or null when unavailable. + public static LatLng getFirstGnssPoint(String filePath) { + File file = new File(filePath); + if (!file.exists()) return null; + + try (FileInputStream fis = new FileInputStream(file)) { + Traj.Trajectory traj = Traj.Trajectory.parseFrom(fis); + List gnssList = traj.getGnssDataList(); + if (!gnssList.isEmpty()) { + Traj.GNSSReading first = gnssList.get(0); + if (first.hasPosition()) { + return new LatLng(first.getPosition().getLatitude(), first.getPosition().getLongitude()); + } + } + } catch (IOException e) { + Log.e(TAG, "Error parsing trajectory file for first GNSS point", e); + } + return null; } -/** 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); + + // Returns the recorded initial position from the trajectory header when available. + public static LatLng getRecordedInitialPoint(String filePath) { + File file = new File(filePath); + if (!file.exists()) return null; + + try (FileInputStream fis = new FileInputStream(file)) { + Traj.Trajectory traj = Traj.Trajectory.parseFrom(fis); + if (traj.hasInitialPosition()) { + double lat = traj.getInitialPosition().getLatitude(); + double lon = traj.getInitialPosition().getLongitude(); + if (isValidLatLon(lat, lon)) { + return new LatLng(lat, lon); + } + } + } catch (IOException e) { + Log.e(TAG, "Error parsing trajectory file for recorded initial point", e); + } + return null; } - 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); + + // Extracts sensor-specific arrays for chart rendering and diagnostics. + public List parse(SensorTypes type) { + List dataList = new ArrayList<>(); + if (trajectory == null) return dataList; + + switch (type) { + case ACCELEROMETER: + for (Traj.IMUReading r : trajectory.getImuDataList()) { + if (r.hasAcc()) dataList.add(new Object[]{r.getRelativeTimestamp(), r.getAcc().getX(), r.getAcc().getY(), r.getAcc().getZ()}); + } + break; + case GYRO: + for (Traj.IMUReading r : trajectory.getImuDataList()) { + if (r.hasGyr()) dataList.add(new Object[]{r.getRelativeTimestamp(), r.getGyr().getX(), r.getGyr().getY(), r.getGyr().getZ()}); + } + break; + case GNSSLATLONG: + for (Traj.GNSSReading r : trajectory.getGnssDataList()) { + if (r.hasPosition()) dataList.add(new Object[]{r.getPosition().getRelativeTimestamp(), r.getPosition().getLatitude(), r.getPosition().getLongitude()}); + } + break; + case WIFI: + for (Traj.Fingerprint fp : trajectory.getWifiFingerprintsList()) { + dataList.add(new Object[]{fp.getRelativeTimestamp(), (float) fp.getRfScansCount()}); + } + break; + case PRESSURE: + for (Traj.BarometerReading r : trajectory.getPressureDataList()) { + dataList.add(new Object[]{r.getRelativeTimestamp(), r.getPressure()}); + } + break; + case LIGHT: + for (Traj.LightReading r : trajectory.getLightDataList()) { + dataList.add(new Object[]{r.getRelativeTimestamp(), r.getLight()}); + } + break; + case PDR: + for (Traj.RelativePosition r : trajectory.getPdrDataList()) { + dataList.add(new Object[]{r.getRelativeTimestamp(), r.getX(), r.getY()}); + } + break; + } + return dataList; } - 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); + + public long getStartTimestamp() { + return trajectory != null ? trajectory.getStartTimestamp() : 0; } - 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/remote/IndoorMapAPI.java b/app/src/main/java/com/openpositioning/PositionMe/data/remote/IndoorMapAPI.java new file mode 100644 index 00000000..345606e9 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/data/remote/IndoorMapAPI.java @@ -0,0 +1,253 @@ +package com.openpositioning.PositionMe.data.remote; + +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +import java.util.Locale; + +// IndoorMapAPI - Handles API calls to fetch indoor map data +// Communicates with servers to get building info, floor plans, etc. +public class IndoorMapAPI { + private static final String TAG = "IndoorMapAPI"; + + // API endpoints - Update these with your actual server endpoints + private static final String BASE_URL = "https://openpositioning.org/api/live"; + + private final OkHttpClient httpClient; + + public IndoorMapAPI() { + this.httpClient = new OkHttpClient(); + } + + // Building data class to represent building information + public static class BuildingInfo { + public String buildingId; + public String buildingName; + public double latitude; + public double longitude; + public List floorNames; + public int floorCount; + + public BuildingInfo() { + this.floorNames = new ArrayList<>(); + } + } + + // Floor plan data class + public static class FloorPlan { + public String floorId; + public String floorName; + public int floorNumber; + public String imageUrl; // URL to floor plan image + public double minLat; + public double maxLat; + public double minLon; + public double maxLon; + } + + // Fetch buildings near a coordinate +// Parameter latitude: User current latitude +// Parameter longitude: User current longitude +// Parameter radiusMeters: Search radius in meters +// Parameter callback: Callback to handle results + public void fetchNearbyBuildings(double latitude, double longitude, double radiusMeters, + BuildingsCallback callback) { + new Thread(() -> { + try { + String url = String.format(Locale.US, "%s/buildings/nearby?lat=%f&lon=%f&radius=%f", + BASE_URL, latitude, longitude, radiusMeters); + + Request request = new Request.Builder() + .url(url) + .get() + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful() || response.body() == null) { + callback.onError("API Error: " + response.code()); + return; + } + + String jsonData = response.body().string(); + try { + List buildings = parseBuildings(jsonData); + callback.onSuccess(buildings); + } catch (JSONException e) { + Log.e(TAG, "JSON parse error", e); + callback.onError("Parse error: " + e.getMessage()); + } + } + } catch (IOException e) { + Log.e(TAG, "Network error", e); + callback.onError("Network error: " + e.getMessage()); + } + }).start(); + } + + // Fetch floor plans for a specific building +// Parameter buildingId: Building ID to fetch floors for +// Parameter callback: Callback to handle results + public void fetchBuildingFloors(String buildingId, FloorsCallback callback) { + new Thread(() -> { + try { + String url = String.format(Locale.US, "%s/buildings/%s/floors", BASE_URL, buildingId); + + Request request = new Request.Builder() + .url(url) + .get() + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful() || response.body() == null) { + callback.onError("API Error: " + response.code()); + return; + } + + String jsonData = response.body().string(); + try { + List floors = parseFloors(jsonData); + callback.onSuccess(floors); + } catch (JSONException e) { + Log.e(TAG, "JSON parse error", e); + callback.onError("Parse error: " + e.getMessage()); + } + } + } catch (IOException e) { + Log.e(TAG, "Network error", e); + callback.onError("Network error: " + e.getMessage()); + } + }).start(); + } + + // Fetch building outline/boundary polygon +// Parameter buildingId: Building ID +// Parameter callback: Callback with coordinate array + public void fetchBuildingOutline(String buildingId, OutlineCallback callback) { + new Thread(() -> { + try { + String url = String.format(Locale.US, "%s/buildings/%s/outline", BASE_URL, buildingId); + + Request request = new Request.Builder() + .url(url) + .get() + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful() || response.body() == null) { + callback.onError("API Error: " + response.code()); + return; + } + + String jsonData = response.body().string(); + try { + double[][] coordinates = parseOutlineCoordinates(jsonData); + callback.onSuccess(coordinates); + } catch (JSONException e) { + Log.e(TAG, "JSON parse error", e); + callback.onError("Parse error: " + e.getMessage()); + } + } + } catch (IOException e) { + Log.e(TAG, "Network error", e); + callback.onError("Network error: " + e.getMessage()); + } + }).start(); + } + + // Parsing methods + + private List parseBuildings(String jsonData) throws JSONException { + List buildings = new ArrayList<>(); + JSONArray array = new JSONArray(jsonData); + + for (int i = 0; i < array.length(); i++) { + JSONObject obj = array.getJSONObject(i); + BuildingInfo building = new BuildingInfo(); + + building.buildingId = obj.optString("building_id", ""); + building.buildingName = obj.optString("name", "Unknown Building"); + building.latitude = obj.optDouble("latitude", 0.0); + building.longitude = obj.optDouble("longitude", 0.0); + building.floorCount = obj.optInt("floor_count", 1); + + // Parse floor names if available + JSONArray floors = obj.optJSONArray("floors"); + if (floors != null) { + for (int j = 0; j < floors.length(); j++) { + building.floorNames.add(floors.getString(j)); + } + } + + buildings.add(building); + } + + return buildings; + } + + private List parseFloors(String jsonData) throws JSONException { + List floors = new ArrayList<>(); + JSONArray array = new JSONArray(jsonData); + + for (int i = 0; i < array.length(); i++) { + JSONObject obj = array.getJSONObject(i); + FloorPlan floor = new FloorPlan(); + + floor.floorId = obj.optString("floor_id", ""); + floor.floorName = obj.optString("name", "Floor " + i); + floor.floorNumber = obj.optInt("floor_number", i); + floor.imageUrl = obj.optString("image_url", ""); + floor.minLat = obj.optDouble("bounds_min_lat", 0.0); + floor.maxLat = obj.optDouble("bounds_max_lat", 0.0); + floor.minLon = obj.optDouble("bounds_min_lon", 0.0); + floor.maxLon = obj.optDouble("bounds_max_lon", 0.0); + + floors.add(floor); + } + + return floors; + } + + private double[][] parseOutlineCoordinates(String jsonData) throws JSONException { + JSONObject obj = new JSONObject(jsonData); + JSONArray coords = obj.getJSONArray("coordinates"); + + double[][] result = new double[coords.length()][2]; + for (int i = 0; i < coords.length(); i++) { + JSONArray coord = coords.getJSONArray(i); + result[i][0] = coord.getDouble(0); // latitude + result[i][1] = coord.getDouble(1); // longitude + } + + return result; + } + + // Callback interfaces + + public interface BuildingsCallback { + void onSuccess(List buildings); + void onError(String error); + } + + public interface FloorsCallback { + void onSuccess(List floors); + void onError(String error); + } + + public interface OutlineCallback { + void onSuccess(double[][] coordinates); + void onError(String error); + } +} + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java b/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java index 7f7e74b2..83b1cdf4 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java +++ b/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java @@ -1,16 +1,4 @@ package com.openpositioning.PositionMe.data.remote; -import android.util.Log; -import java.util.Map; -import java.util.HashMap; -import java.util.Iterator; -import java.io.BufferedReader; -import java.io.FileReader; -import org.json.JSONObject; - -import android.os.Environment; - -import java.io.FileInputStream; -import java.io.OutputStream; import android.content.Context; import android.content.SharedPreferences; @@ -20,62 +8,62 @@ import android.os.Environment; import android.os.Handler; import android.os.Looper; +import android.util.Log; import android.widget.Toast; -import androidx.annotation.NonNull; + import androidx.preference.PreferenceManager; -import com.google.protobuf.util.JsonFormat; import com.openpositioning.PositionMe.BuildConfig; -import com.openpositioning.PositionMe.Traj; -import com.openpositioning.PositionMe.presentation.fragment.FilesFragment; import com.openpositioning.PositionMe.presentation.activity.MainActivity; +import com.openpositioning.PositionMe.presentation.fragment.FilesFragment; import com.openpositioning.PositionMe.sensors.Observable; import com.openpositioning.PositionMe.sensors.Observer; +import org.json.JSONObject; + +import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Files; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; -import java.util.zip.ZipEntry; +import java.util.Map; import java.util.zip.ZipInputStream; import okhttp3.Call; import okhttp3.Callback; -import okhttp3.Headers; import okhttp3.MediaType; import okhttp3.MultipartBody; -import okhttp3.OkHttp; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.ResponseBody; +import com.openpositioning.PositionMe.Traj; -/** - * This class handles communications with the server through HTTPs. The class uses an - * {@link OkHttpClient} for making requests to the server. The class includes methods for sending - * a recorded trajectory, uploading locally-stored trajectories, downloading trajectories from the - * server and requesting information about the uploaded trajectories. - * - * Keys and URLs are hardcoded strings, given the simple and academic nature of the project. - * - * @author Michal Dvorak - * @author Mate Stodulka - */ +// Network gateway for trajectory upload, download, and record metadata persistence. +// This class is responsible for: +// Preparing protobuf payloads for HTTP multipart upload. +// Downloading zipped trajectory archives and extracting the selected entry. +// Storing local download metadata used by file/replay screens. +// Notifying UI observers about request completion state. public class ServerCommunications implements Observable { + private static final String REPLAY_DIAG_TAG = "ReplayDiag"; public static Map downloadRecords = new HashMap<>(); - // Application context for handling permissions and devices private final Context context; - // Network status checking private ConnectivityManager connMgr; private boolean isWifiConn; private boolean isMobileConn; @@ -85,30 +73,24 @@ public class ServerCommunications implements Observable { private boolean success; private List observers; - // Static constants necessary for communications - private static final String userKey = BuildConfig.OPENPOSITIONING_API_KEY; - private static final String masterKey = BuildConfig.OPENPOSITIONING_MASTER_KEY; - private static final String uploadURL = - "https://openpositioning.org/api/live/trajectory/upload/" + userKey - + "/?key=" + masterKey; - private static final String downloadURL = - "https://openpositioning.org/api/live/trajectory/download/" + userKey - + "?skip=0&limit=30&key=" + masterKey; - private static final String infoRequestURL = - "https://openpositioning.org/api/live/users/trajectories/" + userKey - + "?key=" + masterKey; - private static final String PROTOCOL_CONTENT_TYPE = "multipart/form-data"; - private static final String PROTOCOL_ACCEPT_TYPE = "application/json"; + // API keys and endpoint constants. + private static final String RAW_USER_KEY = BuildConfig.OPENPOSITIONING_API_KEY; + private static final String RAW_MASTER_KEY = BuildConfig.OPENPOSITIONING_MASTER_KEY; + + private static final String userKey = RAW_USER_KEY.replace("<", "").replace(">", "").trim(); + private static final String masterKey = RAW_MASTER_KEY.replace("<", "").replace(">", "").trim(); + private static final String BASE_UPLOAD_URL = "https://openpositioning.org/api/live/trajectory/upload/"; + private static final String DOWNLOAD_BASE_URL = + "https://openpositioning.org/api/live/trajectory/download/"; + + private static final String infoRequestURL = + "https://openpositioning.org/api/live/users/trajectories/" + userKey + "?key=" + masterKey; + + private static final String PROTOCOL_CONTENT_TYPE = "multipart/form-data"; + private static final String PROTOCOL_ACCEPT_TYPE = "application/json"; - /** - * Public default constructor of {@link ServerCommunications}. The constructor saves context, - * initialises a {@link ConnectivityManager}, {@link Observer} and gets the user preferences. - * Boolean variables storing WiFi and Mobile Data connection status are initialised to false. - * - * @param context application context for handling permissions and devices. - */ public ServerCommunications(Context context) { this.context = context; this.connMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); @@ -120,233 +102,274 @@ public ServerCommunications(Context context) { this.observers = new ArrayList<>(); } - /** - * Outgoing communication request with a {@link Traj trajectory} object. The recorded - * trajectory is passed to the method. It is processed into the right format for sending - * to the API server. - * - * @param trajectory Traj object matching all the timing and formal restrictions. - */ - public void sendTrajectory(Traj.Trajectory trajectory){ - logDataSize(trajectory); - - // Convert the trajectory to byte array - byte[] binaryTrajectory = trajectory.toByteArray(); - - File path = null; - // for android 13 or higher use dedicated external storage - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - path = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS); + // Uploads one trajectory protobuf to the server. + // campaign is optional; when empty, upload is performed under the user root directory. + public void sendTrajectory(Traj.Trajectory trajectory, String campaign){ + // Build upload URL from campaign scope and user credentials. + String dynamicUrl; + if (campaign != null && !campaign.isEmpty()) { + dynamicUrl = BASE_UPLOAD_URL + campaign + "/" + userKey + "/?key=" + masterKey; + } else { + // No campaign provided: upload to user-level trajectory directory. + dynamicUrl = BASE_UPLOAD_URL + userKey + "/?key=" + masterKey; + } + + Log.e("SERVER_DEBUG", "--------------------------------------------------"); + Log.e("SERVER_DEBUG", "Campaign Passed: " + campaign); + Log.e("SERVER_DEBUG", "Dynamic Upload URL: " + dynamicUrl); + Log.e("SERVER_DEBUG", "--------------------------------------------------"); + + // Guard upload preparation and local file operations to avoid hard crashes. + try { + logDataSize(trajectory); + + // Serialize trajectory protobuf to raw bytes for local staging and HTTP body. + byte[] binaryTrajectory = trajectory.toByteArray(); + Log.e("SERVER_DEBUG", "Trajectory Byte Size: " + binaryTrajectory.length); + + // Choose app-private storage for temporary upload file creation. + File path = null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + path = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS); + } + // Fall back to internal storage when external app directory is unavailable. if (path == null) { path = context.getFilesDir(); } - } else { // for android 12 or lower use internal storage - path = context.getFilesDir(); - } - System.out.println(path.toString()); + if (path == null) { + throw new IOException("Fatal: Could not determine any file storage path."); + } + + System.out.println(path.toString()); - // Format the file name according to date + // Build a timestamped filename from trajectory ID to keep uploads traceable. SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yy-HH-mm-ss"); Date date = new Date(); - File file = new File(path, "trajectory_" + dateFormat.format(date) + ".txt"); - try { - // Write the binary data to the file - FileOutputStream stream = new FileOutputStream(file); - stream.write(binaryTrajectory); - stream.close(); - System.out.println("Recorded binary trajectory for debugging stored in: " + path); - } catch (IOException ee) { - // Catch and print if writing to the file fails - System.err.println("Storing of recorded binary trajectory failed: " + ee.getMessage()); + String safeName = trajectory.getTrajectoryId(); + if (safeName == null || safeName.isEmpty()) { + safeName = "trajectory"; } - // Check connections available before sending data - checkNetworkStatus(); + // Local temp filename format: traj__.txt + String fileName = "traj_" + safeName + "_" + dateFormat.format(date) + ".txt"; - // Check if user preference allows for syncing with mobile data - // TODO: add sync delay and enforce settings - boolean enableMobileData = this.settings.getBoolean("mobile_sync", false); - // Check if device is connected to WiFi or to mobile data with enabled preference - if(this.isWifiConn || (enableMobileData && isMobileConn)) { - // Instantiate client for HTTP requests - OkHttpClient client = new OkHttpClient(); - - // Creaet a equest body with a file to upload in multipart/form-data format - RequestBody requestBody = new MultipartBody.Builder().setType(MultipartBody.FORM) - .addFormDataPart("file", file.getName(), - RequestBody.create(MediaType.parse("text/plain"), file)) - .build(); - - // Create a POST request with the required headers - Request request = new Request.Builder().url(uploadURL).post(requestBody) - .addHeader("accept", PROTOCOL_ACCEPT_TYPE) - .addHeader("Content-Type", PROTOCOL_CONTENT_TYPE).build(); - - // Enqueue the request to be executed asynchronously and handle the response - client.newCall(request).enqueue(new Callback() { - - // Handle failure to get response from the server - @Override public void onFailure(Call call, IOException e) { - e.printStackTrace(); - System.err.println("Failure to get response"); - // Delete the local file and set success to false - //file.delete(); - success = false; - notifyObservers(1); - } + File file = new File(path, fileName); + Log.e("SERVER_DEBUG", "Saving temp file to: " + file.getAbsolutePath()); - private void copyFile(File src, File dst) throws IOException { - try (InputStream in = new FileInputStream(src); - OutputStream out = new FileOutputStream(dst)) { - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - out.write(buf, 0, len); + // Persist serialized payload before network transmission. + FileOutputStream stream = new FileOutputStream(file); + stream.write(binaryTrajectory); + stream.close(); + System.out.println("Recorded binary trajectory stored in: " + path + "/" + fileName); + + checkNetworkStatus(); + + boolean enableMobileData = this.settings.getBoolean("mobile_sync", false); + if(this.isWifiConn || (enableMobileData && isMobileConn)) { + + // Emit payload and routing diagnostics before dispatching HTTP request. + Log.i("SERVER_DEBUG", "Starting upload for file: " + file.getName()); + Log.e("SERVER_DEBUG", "File: " + file.getName()); + Log.e("SERVER_DEBUG", "File Size: " + file.length() + " bytes"); + Log.e("SERVER_DEBUG", "Campaign: " + (campaign != null && !campaign.isEmpty() ? campaign : "[empty - user directory]")); + Log.e("SERVER_DEBUG", "URL: " + dynamicUrl); + Log.e("SERVER_DEBUG", "---- Trajectory Data Statistics ----"); + Log.e("SERVER_DEBUG", "IMU Data count: " + trajectory.getImuDataCount()); + Log.e("SERVER_DEBUG", "Magnetometer Data count: " + trajectory.getMagnetometerDataCount()); + Log.e("SERVER_DEBUG", "Pressure Data count: " + trajectory.getPressureDataCount()); + Log.e("SERVER_DEBUG", "GNSS Data count: " + trajectory.getGnssDataCount()); + Log.e("SERVER_DEBUG", "WiFi Fingerprints count: " + trajectory.getWifiFingerprintsCount()); + Log.e("SERVER_DEBUG", "BLE Data count: " + trajectory.getBleDataCount()); + Log.e("SERVER_DEBUG", "PDR Data count: " + trajectory.getPdrDataCount()); + Log.e("SERVER_DEBUG", "Test Points count: " + trajectory.getTestPointsCount()); + Log.e("SERVER_DEBUG", "========================================================"); + + OkHttpClient client = new OkHttpClient.Builder() + .followRedirects(false) + .followSslRedirects(false) + .build(); + + // Send file content as binary stream in multipart form-data. + RequestBody requestBody = new MultipartBody.Builder().setType(MultipartBody.FORM) + .addFormDataPart("file", file.getName(), + RequestBody.create(MediaType.parse("application/octet-stream"), file)) + .build(); + + Request request = new Request.Builder() + .url(dynamicUrl) + .post(requestBody) + .addHeader("accept", PROTOCOL_ACCEPT_TYPE) + .build(); + + client.newCall(request).enqueue(new Callback() { + @Override public void onFailure(Call call, IOException e) { + e.printStackTrace(); + Log.e("ServerCommunications", "Upload failed: " + e.getMessage()); + Log.e("SERVER_DEBUG", "Exception Type: " + e.getClass().getSimpleName()); + Log.e("SERVER_DEBUG", "Error Message: " + e.getMessage()); + Log.e("SERVER_DEBUG", "Stack Trace:"); + for (StackTraceElement element : e.getStackTrace()) { + Log.e("SERVER_DEBUG", " " + element.toString()); } + Log.e("SERVER_DEBUG", "========================================================"); + success = false; + notifyObservers(1); } - } - // Process the server's response - @Override public void onResponse(Call call, Response response) throws IOException { - try (ResponseBody responseBody = response.body()) { - // If the response is unsuccessful, delete the local file and throw an - // exception - if (!response.isSuccessful()) { - //file.delete(); -// System.err.println("POST error response: " + responseBody.string()); - - String errorBody = responseBody.string(); - infoResponse = "Upload failed: " + errorBody; - new Handler(Looper.getMainLooper()).post(() -> - Toast.makeText(context, infoResponse, Toast.LENGTH_SHORT).show()); // show error message to users - - System.err.println("POST error response: " + errorBody); - success = false; + @Override public void onResponse(Call call, Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + Log.e("SERVER_DEBUG", ">>> Response Code: " + response.code()); + Log.e("SERVER_DEBUG", ">>> Response Message: " + response.message()); + + // Log response headers for troubleshooting upload failures. + Log.e("SERVER_DEBUG", ">>> Response Headers:"); + for (String headerName : response.headers().names()) { + Log.e("SERVER_DEBUG", " " + headerName + ": " + response.headers().get(headerName)); + } + + if (!response.isSuccessful()) { + String errorBody = responseBody.string(); + infoResponse = "Upload failed (" + response.code() + "): " + errorBody; + + // Log status and server-provided error body for diagnostics. + Log.e("SERVER_DEBUG", "Upload Response Code: " + response.code()); + Log.e("SERVER_DEBUG", "Error Body: " + errorBody); + + new Handler(Looper.getMainLooper()).post(() -> + Toast.makeText(context, infoResponse, Toast.LENGTH_SHORT).show()); + success = false; + notifyObservers(1); + return; + } + + // Upload succeeded; keep response for debug visibility. + System.out.println("Successful post response: " + responseBody.string()); + Log.d("SERVER_DEBUG", "UPLOAD SUCCESS!"); + + // Duplicate success print kept to preserve current runtime behavior. + System.out.println("Successful post response: " + responseBody.string()); + Log.d("SERVER_DEBUG", "UPLOAD SUCCESS!"); + + // Copy uploaded file to public Downloads for manual inspection. + File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + File downloadFile = new File(downloadsDir, file.getName()); + try { + copyFile(file, downloadFile); + } catch (IOException e) { + e.printStackTrace(); + } + + success = file.delete(); notifyObservers(1); - throw new IOException("Unexpected code " + response); } + } - // Print the response headers - Headers responseHeaders = response.headers(); - for (int i = 0, size = responseHeaders.size(); i < size; i++) { - System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i)); - } - // Print a confirmation of a successful POST to API - System.out.println("Successful post response: " + responseBody.string()); - - System.out.println("Get file: " + file.getName()); - String originalPath = file.getAbsolutePath(); - System.out.println("Original trajectory file saved at: " + originalPath); - - // Copy the file to the Downloads folder - File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); - File downloadFile = new File(downloadsDir, file.getName()); - try { - copyFile(file, downloadFile); - System.out.println("Trajectory file copied to Downloads: " + downloadFile.getAbsolutePath()); - } catch (IOException e) { - e.printStackTrace(); - System.err.println("Failed to copy file to Downloads: " + e.getMessage()); + private void copyFile(File src, File dst) throws IOException { + try (InputStream in = new FileInputStream(src); + OutputStream out = new FileOutputStream(dst)) { + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } } - - // Delete local file and set success to true - success = file.delete(); - notifyObservers(1); } - } - }); + }); + } else { + Log.e("SERVER_DEBUG", "No Network Connection available for upload."); + success = false; + notifyObservers(1); + } + + } catch (Exception e) { + // Last-line protection for unexpected exceptions in upload preparation path. + Log.e("SERVER_DEBUG", "CRITICAL ERROR during sendTrajectory: ", e); + e.printStackTrace(); + + new Handler(Looper.getMainLooper()).post(() -> + Toast.makeText(context, "Error: " + e.getMessage(), Toast.LENGTH_LONG).show()); } - else { - // If the device is not connected to network or allowed to send, do not send trajectory - // and notify observers and user - System.err.println("No uploading allowed right now!"); + } + + public void uploadLocalTrajectory(File localTrajectory, String campaign) { + // Validate selected local file before creating HTTP multipart request. + if (localTrajectory == null || !localTrajectory.exists() || !localTrajectory.isFile()) { + String missingPath = (localTrajectory == null) ? "null" : localTrajectory.getAbsolutePath(); + infoResponse = "Upload failed: local trajectory file not found: " + missingPath; + Log.e("SERVER_DEBUG", infoResponse); success = false; notifyObservers(1); + new Handler(Looper.getMainLooper()).post(() -> + Toast.makeText(context, infoResponse, Toast.LENGTH_LONG).show()); + return; } - } - /** - * Uploads a local trajectory file to the API server in the specified format. - * {@link OkHttp} library is used for the asynchronous POST request. - * - * @param localTrajectory the File object of the local trajectory to be uploaded - */ - public void uploadLocalTrajectory(File localTrajectory) { + String dynamicUrl; + if (campaign != null && !campaign.isEmpty()) { + dynamicUrl = BASE_UPLOAD_URL + campaign + "/" + userKey + "/?key=" + masterKey; + } else { + // No campaign provided: upload to user-level trajectory directory. + dynamicUrl = BASE_UPLOAD_URL + userKey + "/?key=" + masterKey; + } - // Instantiate client for HTTP requests - OkHttpClient client = new OkHttpClient(); + Log.e("SERVER_DEBUG", "Local Upload URL: " + dynamicUrl); + Log.e("SERVER_DEBUG", "Local file exists: " + localTrajectory.exists() + ", path=" + localTrajectory.getAbsolutePath()); + + OkHttpClient client = new OkHttpClient.Builder() + .followRedirects(false) + .followSslRedirects(false) + .build(); - // robustness improvement RequestBody fileRequestBody; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { try { byte[] fileBytes = Files.readAllBytes(localTrajectory.toPath()); - fileRequestBody = RequestBody.create(MediaType.parse("text/plain"), fileBytes); + fileRequestBody = RequestBody.create(MediaType.parse("application/octet-stream"), fileBytes); } catch (IOException e) { e.printStackTrace(); - // if failed, use File object to construct RequestBody - fileRequestBody = RequestBody.create(MediaType.parse("text/plain"), localTrajectory); + // Keep upload available on API levels where readAllBytes may fail on file handles. + Log.e("SERVER_DEBUG", "readAllBytes failed for: " + localTrajectory.getAbsolutePath() + ", fallback to file stream body", e); + fileRequestBody = RequestBody.create(MediaType.parse("application/octet-stream"), localTrajectory); } } else { - fileRequestBody = RequestBody.create(MediaType.parse("text/plain"), localTrajectory); + fileRequestBody = RequestBody.create(MediaType.parse("application/octet-stream"), localTrajectory); } - // Create request body with a file to upload in multipart/form-data format RequestBody requestBody = new MultipartBody.Builder().setType(MultipartBody.FORM) .addFormDataPart("file", localTrajectory.getName(), fileRequestBody) .build(); - // Create a POST request with the required headers - okhttp3.Request request = new okhttp3.Request.Builder().url(uploadURL).post(requestBody) + Request request = new Request.Builder() + .url(dynamicUrl) + .post(requestBody) .addHeader("accept", PROTOCOL_ACCEPT_TYPE) - .addHeader("Content-Type", PROTOCOL_CONTENT_TYPE).build(); + .build(); - // Enqueue the request to be executed asynchronously and handle the response client.newCall(request).enqueue(new okhttp3.Callback() { @Override public void onFailure(Call call, IOException e) { - // Print error message, set success to false and notify observers e.printStackTrace(); -// localTrajectory.delete(); success = false; - System.err.println("UPLOAD: Failure to get response"); notifyObservers(1); - infoResponse = "Upload failed: " + e.getMessage(); // Store error message + infoResponse = "Upload failed: " + e.getMessage(); new Handler(Looper.getMainLooper()).post(() -> - Toast.makeText(context, infoResponse, Toast.LENGTH_SHORT).show()); // show error message to users + Toast.makeText(context, infoResponse, Toast.LENGTH_SHORT).show()); } @Override public void onResponse(Call call, Response response) throws IOException { try (ResponseBody responseBody = response.body()) { if (!response.isSuccessful()) { - // Print error message, set success to false and throw an exception success = false; -// System.err.println("UPLOAD unsuccessful: " + responseBody.string()); notifyObservers(1); -// localTrajectory.delete(); - assert responseBody != null; String errorBody = responseBody.string(); - System.err.println("UPLOAD unsuccessful: " + errorBody); infoResponse = "Upload failed: " + errorBody; new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(context, infoResponse, Toast.LENGTH_SHORT).show()); - throw new IOException("UPLOAD failed with code " + response); - } - - // Print the response headers - Headers responseHeaders = response.headers(); - for (int i = 0, size = responseHeaders.size(); i < size; i++) { - System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i)); + return; } - - // Print a confirmation of a successful POST to API - assert responseBody != null; - System.out.println("UPLOAD SUCCESSFUL: " + responseBody.string()); - - // Delete local file, set success to true and notify observers success = localTrajectory.delete(); notifyObservers(1); } @@ -354,15 +377,10 @@ public void onResponse(Call call, Response response) throws IOException { }); } - /** - * Loads download records from a JSON file and updates the downloadRecords map. - * If the file exists, it reads the JSON content and populates the map. - */ private void loadDownloadRecords() { - // Point to the app-specific Downloads folder + // Load locally cached download metadata into memory for quick lookup by id. File recordsDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS); File recordsFile = new File(recordsDir, "download_records.json"); - if (recordsFile.exists()) { try (BufferedReader reader = new BufferedReader(new FileReader(recordsFile))) { StringBuilder json = new StringBuilder(); @@ -370,7 +388,6 @@ private void loadDownloadRecords() { while ((line = reader.readLine()) != null) { json.append(line); } - JSONObject jsonObject = new JSONObject(json.toString()); for (Iterator it = jsonObject.keys(); it.hasNext(); ) { String key = it.next(); @@ -378,239 +395,198 @@ private void loadDownloadRecords() { JSONObject record = jsonObject.getJSONObject(key); String id = record.getString("id"); downloadRecords.put(id, record); - } catch (Exception e) { - System.err.println("Error loading record with key: " + key); - e.printStackTrace(); - } + } catch (Exception e) {} } - - System.out.println("Loaded downloadRecords: " + downloadRecords); - } catch (Exception e) { e.printStackTrace(); } - } else { - System.out.println("Download_records.json not found in app-specific directory."); } } - /** - * Saves a download record to a JSON file. - * The method creates or updates the JSON file with the provided details. - * - * @param startTimestamp the start timestamp of the trajectory - * @param fileName the name of the file - * @param id the ID of the trajectory - * @param dateSubmitted the date the trajectory was submitted - */ private void saveDownloadRecord(long startTimestamp, String fileName, String id, String dateSubmitted) { + // Persist one downloaded trajectory entry to download_records.json. File recordsDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS); File recordsFile = new File(recordsDir, "download_records.json"); JSONObject jsonObject; - try { - // Ensure the directory exists - if (recordsDir != null && !recordsDir.exists()) { - recordsDir.mkdirs(); - } - - // If the file does not exist, create it + if (recordsDir != null && !recordsDir.exists()) recordsDir.mkdirs(); if (!recordsFile.exists()) { - if (recordsFile.createNewFile()) { - jsonObject = new JSONObject(); - } else { - System.err.println("Failed to create file: " + recordsFile.getAbsolutePath()); - return; - } + if (recordsFile.createNewFile()) jsonObject = new JSONObject(); + else return; } else { - // Read the existing contents StringBuilder jsonBuilder = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new FileReader(recordsFile))) { String line; - while ((line = reader.readLine()) != null) { - jsonBuilder.append(line); - } + while ((line = reader.readLine()) != null) jsonBuilder.append(line); } - // If file is empty or invalid JSON, use a fresh JSONObject - jsonObject = jsonBuilder.length() > 0 - ? new JSONObject(jsonBuilder.toString()) - : new JSONObject(); + jsonObject = jsonBuilder.length() > 0 ? new JSONObject(jsonBuilder.toString()) : new JSONObject(); } - - // Create the new record details JSONObject recordDetails = new JSONObject(); recordDetails.put("file_name", fileName); recordDetails.put("startTimeStamp", startTimestamp); recordDetails.put("date_submitted", dateSubmitted); recordDetails.put("id", id); - - // Insert or update in the main JSON jsonObject.put(id, recordDetails); - - // Write updated JSON to file try (FileWriter writer = new FileWriter(recordsFile)) { writer.write(jsonObject.toString(4)); writer.flush(); } - - System.out.println("Download record saved successfully at: " + recordsFile.getAbsolutePath()); - } catch (Exception e) { e.printStackTrace(); - System.err.println("Error saving download record: " + e.getMessage()); } } - /** - * Perform API request for downloading a Trajectory uploaded to the server. The trajectory is - * retrieved from a zip file, with the method accepting a position argument specifying the - * trajectory to be downloaded. The trajectory is then converted to a protobuf object and - * then to a JSON string to be downloaded to the device's Downloads folder. - * - * @param position the position of the trajectory in the zip file to retrieve - * @param id the ID of the trajectory - * @param dateSubmitted the date the trajectory was submitted - */ public void downloadTrajectory(int position, String id, String dateSubmitted) { - loadDownloadRecords(); // Load existing records from app-specific directory + if (position < 0) { + Log.e(REPLAY_DIAG_TAG, "status=FILE_BAD reason=INVALID_POSITION position=" + position + " id=" + id); + new Handler(Looper.getMainLooper()).post(() -> + Toast.makeText(context, "Download failed: invalid trajectory index.", Toast.LENGTH_LONG).show()); + return; + } - // Initialise OkHttp client - OkHttpClient client = new OkHttpClient(); + // Request enough archive entries so the selected index is guaranteed to be in range. + int requiredLimit = Math.max(position + 1, 30); + String dynamicDownloadUrl = DOWNLOAD_BASE_URL + userKey + + "?skip=0&limit=" + requiredLimit + "&key=" + masterKey; - // Create GET request with required header + loadDownloadRecords(); + OkHttpClient client = new OkHttpClient(); okhttp3.Request request = new okhttp3.Request.Builder() - .url(downloadURL) + .url(dynamicDownloadUrl) .addHeader("accept", PROTOCOL_ACCEPT_TYPE) .get() .build(); - // Enqueue the GET request for asynchronous execution + Log.i(REPLAY_DIAG_TAG, "status=DOWNLOAD_REQUEST position=" + position + " limit=" + requiredLimit + " id=" + id); + client.newCall(request).enqueue(new okhttp3.Callback() { - @Override - public void onFailure(Call call, IOException e) { - e.printStackTrace(); - } + @Override public void onFailure(Call call, IOException e) { e.printStackTrace(); } - @Override - public void onResponse(Call call, Response response) throws IOException { + @Override public void onResponse(Call call, Response response) throws IOException { try (ResponseBody responseBody = response.body()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); - // Extract the nth entry from the zip + // Open zip response and move to the selected entry index. InputStream inputStream = responseBody.byteStream(); ZipInputStream zipInputStream = new ZipInputStream(inputStream); - java.util.zip.ZipEntry zipEntry; int zipCount = 0; while ((zipEntry = zipInputStream.getNextEntry()) != null) { - if (zipCount == position) { - // break if zip entry position matches the desired position - break; - } + if (zipCount == position) break; zipCount++; } - // Initialise a byte array output stream - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + if (zipEntry == null) { + Log.e(REPLAY_DIAG_TAG, "status=FILE_BAD reason=DOWNLOAD_ZIP_ENTRY_NOT_FOUND position=" + position + " id=" + id); + new Handler(Looper.getMainLooper()).post(() -> + Toast.makeText(context, "Download failed: selected trajectory data not found in archive.", Toast.LENGTH_LONG).show()); + zipInputStream.close(); + inputStream.close(); + return; + } - // Read the zipped data and write it to the byte array output stream + // Read selected zip entry into memory for protobuf parsing and file write. + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = zipInputStream.read(buffer)) != -1) { byteArrayOutputStream.write(buffer, 0, bytesRead); } + byte[] byteArray = byteArrayOutputStream.toByteArray(); + if (byteArray.length == 0) { + Log.e(REPLAY_DIAG_TAG, "status=FILE_BAD reason=DOWNLOAD_EMPTY_PAYLOAD entry=" + zipEntry.getName() + " id=" + id); + new Handler(Looper.getMainLooper()).post(() -> + Toast.makeText(context, "Download failed: trajectory payload is empty.", Toast.LENGTH_LONG).show()); + zipInputStream.closeEntry(); + byteArrayOutputStream.close(); + zipInputStream.close(); + inputStream.close(); + return; + } - // Convert the byte array to protobuf - byte[] byteArray = byteArrayOutputStream.toByteArray(); + // Parse protobuf and verify the payload contains replayable trajectory data. Traj.Trajectory receivedTrajectory = Traj.Trajectory.parseFrom(byteArray); + boolean hasReplayData = receivedTrajectory.getPdrDataCount() > 0 + || receivedTrajectory.getGnssDataCount() > 0 + || receivedTrajectory.hasInitialPosition(); + + Log.i(REPLAY_DIAG_TAG, + "status=DOWNLOAD_PARSED id=" + id + + " entry=" + zipEntry.getName() + + " bytes=" + byteArray.length + + " pdr=" + receivedTrajectory.getPdrDataCount() + + " gnss=" + receivedTrajectory.getGnssDataCount() + + " wifi_fp=" + receivedTrajectory.getWifiFingerprintsCount() + + " initial=" + receivedTrajectory.hasInitialPosition()); + + if (!hasReplayData) { + Log.e(REPLAY_DIAG_TAG, "status=FILE_NO_REPLAY_DATA reason=DOWNLOADED_TRAJECTORY_EMPTY id=" + id); + new Handler(Looper.getMainLooper()).post(() -> + Toast.makeText(context, "Downloaded trajectory contains no replayable data.", Toast.LENGTH_LONG).show()); + zipInputStream.closeEntry(); + byteArrayOutputStream.close(); + zipInputStream.close(); + inputStream.close(); + return; + } - // Inspect the size of the received trajectory - logDataSize(receivedTrajectory); - - // Print a message in the console long startTimestamp = receivedTrajectory.getStartTimestamp(); - String fileName = "trajectory_" + dateSubmitted + ".txt"; - // Place the file in your app-specific "Downloads" folder + // Use a deterministic local filename for replay screen lookup. + String fileName = "trajectory_" + dateSubmitted + ".protobuf"; + File appSpecificDownloads = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS); if (appSpecificDownloads != null && !appSpecificDownloads.exists()) { appSpecificDownloads.mkdirs(); } File file = new File(appSpecificDownloads, fileName); - try (FileWriter fileWriter = new FileWriter(file)) { - String receivedTrajectoryString = JsonFormat.printer().print(receivedTrajectory); - fileWriter.write(receivedTrajectoryString); - fileWriter.flush(); - System.err.println("Received trajectory stored in: " + file.getAbsolutePath()); + + // Write raw protobuf bytes without text conversion. + try (FileOutputStream fos = new FileOutputStream(file)) { + fos.write(byteArray); + fos.flush(); } catch (IOException ee) { System.err.println("Trajectory download failed"); + ee.printStackTrace(); } finally { - // Close all streams and entries to release resources zipInputStream.closeEntry(); byteArrayOutputStream.close(); zipInputStream.close(); inputStream.close(); } - // Save the download record + // Update local metadata cache after successful file write. saveDownloadRecord(startTimestamp, fileName, id, dateSubmitted); loadDownloadRecords(); } } }); - } - /** - * API request for information about submitted trajectories. If the response is successful, - * the {@link ServerCommunications#infoResponse} field is updated and observes notified. - * - */ public void sendInfoRequest() { - // Create a new OkHttpclient OkHttpClient client = new OkHttpClient(); - - // Create GET info request with appropriate URL and header okhttp3.Request request = new okhttp3.Request.Builder() .url(infoRequestURL) .addHeader("accept", PROTOCOL_ACCEPT_TYPE) .get() .build(); - // Enqueue the GET request for asynchronous execution client.newCall(request).enqueue(new okhttp3.Callback() { - @Override public void onFailure(Call call, IOException e) { - e.printStackTrace(); - } - + @Override public void onFailure(Call call, IOException e) { e.printStackTrace(); } @Override public void onResponse(Call call, Response response) throws IOException { try (ResponseBody responseBody = response.body()) { - // Check if the response is successful - if (!response.isSuccessful()) throw new IOException("Unexpected code " + - response); - - // Get the requested information from the response body and save it in a string - // TODO: add printing to the screen somewhere + if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); infoResponse = responseBody.string(); - // Print a message in the console and notify observers - System.out.println("Response received"); notifyObservers(0); } } }); } - /** - * This method checks the device's connection status. It sets boolean variables depending on - * the type of active network connection. - */ private void checkNetworkStatus() { - // Get active network information NetworkInfo activeInfo = connMgr.getActiveNetworkInfo(); - - // Check for active connection and set flags accordingly if (activeInfo != null && activeInfo.isConnected()) { isWifiConn = activeInfo.getType() == ConnectivityManager.TYPE_WIFI; isMobileConn = activeInfo.getType() == ConnectivityManager.TYPE_MOBILE; @@ -620,39 +596,42 @@ private void checkNetworkStatus() { } } - private void logDataSize(Traj.Trajectory trajectory) { + Log.i("ServerCommunications", "========== TRAJECTORY DATA SIZE =========="); Log.i("ServerCommunications", "IMU Data size: " + trajectory.getImuDataCount()); - Log.i("ServerCommunications", "Position Data size: " + trajectory.getPositionDataCount()); + Log.i("ServerCommunications", "Magnetometer Data size: " + trajectory.getMagnetometerDataCount()); Log.i("ServerCommunications", "Pressure Data size: " + trajectory.getPressureDataCount()); Log.i("ServerCommunications", "Light Data size: " + trajectory.getLightDataCount()); - Log.i("ServerCommunications", "GNSS Data size: " + trajectory.getGnssDataCount()); - Log.i("ServerCommunications", "WiFi Data size: " + trajectory.getWifiDataCount()); + Log.i("ServerCommunications", "Proximity Data size: " + trajectory.getProximityDataCount()); + + // Highlight GNSS and PDR counts because replay path quality depends on them. + int gnssCount = trajectory.getGnssDataCount(); + int pdrCount = trajectory.getPdrDataCount(); + + if (gnssCount > 0) { + Log.i("ServerCommunications", "GNSS Data size: " + gnssCount); + } else { + Log.e("ServerCommunications", "GNSS Data size: 0 (No GNSS data)"); + } + + if (pdrCount > 0) { + Log.i("ServerCommunications", "PDR Data size: " + pdrCount); + } else { + Log.w("ServerCommunications", "PDR Data size: 0 (No PDR data)"); + } + + Log.i("ServerCommunications", "WiFi Fingerprints size: " + trajectory.getWifiFingerprintsCount()); + Log.i("ServerCommunications", "BLE Data size: " + trajectory.getBleDataCount()); Log.i("ServerCommunications", "APS Data size: " + trajectory.getApsDataCount()); - Log.i("ServerCommunications", "PDR Data size: " + trajectory.getPdrDataCount()); + Log.i("ServerCommunications", "Test Points size: " + trajectory.getTestPointsCount()); + Log.i("ServerCommunications", "=========================================="); } - /** - * {@inheritDoc} - * - * Implement default method from Observable Interface to add new observers to the list of - * registered observers. - * - * @param o Classes which implement the Observer interface to receive updates from the class. - */ @Override public void registerObserver(Observer o) { this.observers.add(o); } - /** - * {@inheritDoc} - * - * Method for notifying all registered observers. The observer is notified based on the index - * passed to the method. - * - * @param index Index for identifying the observer to be notified. - */ @Override public void notifyObservers(int index) { for(Observer o : observers) { @@ -664,4 +643,5 @@ else if (index == 1 && o instanceof MainActivity) { } } } -} \ No newline at end of file +} + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/activity/MainActivity.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/activity/MainActivity.java index 995f010d..b1e331c1 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/activity/MainActivity.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/activity/MainActivity.java @@ -1,7 +1,7 @@ package com.openpositioning.PositionMe.presentation.activity; + import android.Manifest; import android.content.SharedPreferences; - import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; @@ -17,7 +17,6 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.widget.Toolbar; - import androidx.core.content.ContextCompat; import androidx.navigation.NavController; import androidx.navigation.NavOptions; @@ -27,43 +26,21 @@ import androidx.preference.PreferenceManager; import com.openpositioning.PositionMe.R; -import com.openpositioning.PositionMe.data.remote.ServerCommunications; import com.openpositioning.PositionMe.presentation.fragment.HomeFragment; import com.openpositioning.PositionMe.presentation.fragment.SettingsFragment; import com.openpositioning.PositionMe.sensors.Observer; import com.openpositioning.PositionMe.sensors.SensorFusion; import com.openpositioning.PositionMe.utils.PermissionManager; - +// Import required for file saving +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; import java.util.Objects; -/** - * The Main Activity of the application, handling setup, permissions and starting all other fragments - * and processes. - * The Main Activity takes care of most essential tasks before the app can run. Such as setting up - * the views, and enforcing light mode so the colour scheme is consistent. It initialises the - * various fragments and the navigation between them, getting the Navigation controller. It also - * loads the custom action bar with the set theme and icons, and enables back-navigation. The shared - * preferences are also loaded. - *

- * The most important task of the main activity is check and asking for the necessary permissions to - * enable the application to use the required hardware devices. This is done through a number of - * functions that call the OS, as well as pop-up messages warning the user if permissions are denied. - *

- * Once all permissions are granted, the Main Activity obtains the Sensor Fusion instance and sets - * the context, enabling the Fragments to interact with the class without setting it up again. - * - * @see HomeFragment the initial fragment displayed. - * @see com.openpositioning.PositionMe.R.navigation the navigation graph. - * @see SensorFusion the singletion data processing class. - * - * @author Mate Stodulka - * @author Virginia Cangelosi - */ public class MainActivity extends AppCompatActivity implements Observer { - - //region Instance variables + // region Instance variables private NavController navController; private ActivityResultLauncher locationPermissionLauncher; private ActivityResultLauncher multiplePermissionsLauncher; @@ -75,29 +52,19 @@ public class MainActivity extends AppCompatActivity implements Observer { private PermissionManager permissionManager; private static final int PERMISSION_REQUEST_CODE = 100; + // endregion - //endregion - - //region Activity Lifecycle - - /** - * {@inheritDoc} - * Forces light mode, sets up the navigation graph, initialises the toolbar with back action on - * the nav controller, loads the shared preferences and checks for all permissions necessary. - * Sets up a Handler for displaying messages from other classes. - */ + // region Activity Lifecycle @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); setContentView(R.layout.activity_main); - // Set up navigation and fragments NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager() .findFragmentById(R.id.nav_host_fragment); navController = Objects.requireNonNull(navHostFragment).getNavController(); - // Set action bar Toolbar toolbar = findViewById(R.id.main_toolbar); setSupportActionBar(toolbar); toolbar.showOverflowMenu(); @@ -105,77 +72,55 @@ protected void onCreate(Bundle savedInstanceState) { toolbar.setTitleTextColor(ContextCompat.getColor(getApplicationContext(), R.color.black)); toolbar.setNavigationIcon(R.drawable.ic_baseline_back_arrow); - // Set up back action with NavigationUI AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build(); NavigationUI.setupWithNavController(toolbar, navController, appBarConfiguration); - // Get handle for settings this.settings = PreferenceManager.getDefaultSharedPreferences(this); settings.edit().putBoolean("permanentDeny", false).apply(); - // Initialize SensorFusion early so that its context is set this.sensorFusion = SensorFusion.getInstance(); this.sensorFusion.setContext(getApplicationContext()); - // Register multiple permissions launcher multiplePermissionsLauncher = registerForActivityResult( new ActivityResultContracts.RequestMultiplePermissions(), result -> { boolean locationGranted = result.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false); boolean activityGranted = result.getOrDefault(Manifest.permission.ACTIVITY_RECOGNITION, false); + boolean bluetoothScanGranted = Build.VERSION.SDK_INT < Build.VERSION_CODES.S || + result.getOrDefault(Manifest.permission.BLUETOOTH_SCAN, false); + boolean bluetoothConnectGranted = Build.VERSION.SDK_INT < Build.VERSION_CODES.S || + result.getOrDefault(Manifest.permission.BLUETOOTH_CONNECT, false); - if (locationGranted && activityGranted) { - // Both permissions granted + if (locationGranted && activityGranted && bluetoothScanGranted && bluetoothConnectGranted) { allPermissionsObtained(); } else { - // Permission denied Toast.makeText(this, - "Location or Physical Activity permission denied. Some features may not work.", + "Location, activity, or Bluetooth permission denied. Some features may not work.", Toast.LENGTH_LONG).show(); } } ); - // Handler for global toasts and popups from other classes this.httpResponseHandler = new Handler(); } - - - - /** - * {@inheritDoc} - */ @Override public void onPause() { super.onPause(); - - //Ensure sensorFusion has been initialised before unregistering listeners if(sensorFusion != null) { -// sensorFusion.stopListening(); + // sensorFusion.stopListening(); } } - /** - * {@inheritDoc} - * Checks for activities in case the app was closed without granting them, or if they were - * granted through the settings page. Repeats the startup checks done in - * {@link MainActivity#onCreate(Bundle)}. Starts listening in the SensorFusion class. - * - * @see SensorFusion the main data processing class. - */ @Override public void onResume() { super.onResume(); - if (getSupportActionBar() != null) { getSupportActionBar().show(); } - // Delay permission check slightly to ensure the Activity is in the foreground new Handler().postDelayed(() -> { if (isActivityVisible()) { - // Check if both permissions are granted boolean locationGranted = ContextCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION ) == PackageManager.PERMISSION_GRANTED; @@ -183,21 +128,30 @@ public void onResume() { boolean activityGranted = ContextCompat.checkSelfPermission( this, Manifest.permission.ACTIVITY_RECOGNITION ) == PackageManager.PERMISSION_GRANTED; - - if (!locationGranted || !activityGranted) { - // Request both permissions using ActivityResultLauncher - multiplePermissionsLauncher.launch(new String[]{ - Manifest.permission.ACCESS_FINE_LOCATION - }); - multiplePermissionsLauncher.launch(new String[]{ - Manifest.permission.ACTIVITY_RECOGNITION - }); + boolean bluetoothScanGranted = Build.VERSION.SDK_INT < Build.VERSION_CODES.S || + ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED; + boolean bluetoothConnectGranted = Build.VERSION.SDK_INT < Build.VERSION_CODES.S || + ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED; + + if (!locationGranted || !activityGranted || !bluetoothScanGranted || !bluetoothConnectGranted) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + multiplePermissionsLauncher.launch(new String[]{ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACTIVITY_RECOGNITION, + Manifest.permission.BLUETOOTH_SCAN, + Manifest.permission.BLUETOOTH_CONNECT + }); + } else { + multiplePermissionsLauncher.launch(new String[]{ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACTIVITY_RECOGNITION + }); + } } else { - // Both permissions are already granted allPermissionsObtained(); } } - }, 300); // Delay ensures activity is fully visible before requesting permissions + }, 300); if (sensorFusion != null) { sensorFusion.resumeListening(); @@ -208,61 +162,27 @@ private boolean isActivityVisible() { return !isFinishing() && !isDestroyed(); } - - - /** - * Unregisters sensor listeners when the app closes. Not in {@link MainActivity#onPause()} to - * enable recording data with a locked screen. - * - * @see SensorFusion the main data processing class. - */ @Override protected void onDestroy() { if (sensorFusion != null) { -// sensorFusion.stopListening(); // suspended due to the need to record data with -// a locked screen or cross activity + // sensorFusion.stopListening(); } super.onDestroy(); } + // endregion - - //endregion - - //region Permissions - - /** - * Prepares global resources when all permissions are granted. - * Resets the permissions tracking boolean in shared preferences, and initialises the - * {@link SensorFusion} class with the application context, and registers the main activity to - * listen for server responses that SensorFusion receives. - * - * @see SensorFusion the main data processing class. - * @see ServerCommunications the communication class sending and recieving data from the server. - */ + // region Permissions private void allPermissionsObtained() { - // Reset any permission denial flag in SharedPreferences if needed. settings.edit().putBoolean("permanentDeny", false).apply(); - - // Ensure SensorFusion is initialized with a valid context. if (this.sensorFusion == null) { this.sensorFusion = SensorFusion.getInstance(); this.sensorFusion.setContext(getApplicationContext()); } sensorFusion.registerForServerUpdate(this); } + // endregion - - - - //endregion - - //region Navigation - - /** - * {@inheritDoc} - * Sets desired animations and navigates to {@link SettingsFragment} - * when the settings wheel in the action bar is clicked. - */ + // region Navigation @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { if(Objects.requireNonNull(navController.getCurrentDestination()).getId() == item.getItemId()) @@ -279,35 +199,19 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) { } } - /** - * {@inheritDoc} - * Enables navigating back between fragments. - */ @Override public boolean onSupportNavigateUp() { return navController.navigateUp() || super.onSupportNavigateUp(); } - /** - * {@inheritDoc} - * Inflate the designed menu view. - * - * @see com.openpositioning.PositionMe.R.menu for the xml file. - */ @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_items, menu); return true; } - /** - * {@inheritDoc} - * Handles the back button press. If the current fragment is the HomeFragment, a dialog is - * displayed to confirm the exit. If not, the default back navigation is performed. - */ @Override public void onBackPressed() { - // Check if the current destination is HomeFragment (assumed to be the root) if (navController.getCurrentDestination() != null && navController.getCurrentDestination().getId() == R.id.homeFragment) { new AlertDialog.Builder(this) @@ -315,27 +219,18 @@ public void onBackPressed() { .setMessage("Are you sure you want to exit the app?") .setPositiveButton("Yes", (dialog, which) -> { dialog.dismiss(); - finish(); // Close the activity (exit the app) + finish(); }) .setNegativeButton("No", (dialog, which) -> dialog.dismiss()) .create() .show(); } else { - // If not on the root destination, perform the default back navigation. super.onBackPressed(); } } + // endregion - - - //endregion - - //region Global toasts - - /** - * {@inheritDoc} - * Calls the corresponding handler that runs a toast on the Main UI thread. - */ + // region Global toasts @Override public void update(Object[] objList) { assert objList[0] instanceof Boolean; @@ -347,20 +242,57 @@ public void update(Object[] objList) { } } - /** - * Task that displays positive toast on the main UI thread. - * Called when {@link ServerCommunications} successfully uploads a trajectory. - */ private final Runnable displayToastTaskSuccess = () -> Toast.makeText(MainActivity.this, "Trajectory uploaded", Toast.LENGTH_SHORT).show(); - /** - * Task that displays negative toast on the main UI thread. - * Called when {@link ServerCommunications} fails to upload a trajectory. - */ private final Runnable displayToastTaskFailure = () -> { -// Toast.makeText(MainActivity.this, "Failed to complete trajectory upload", Toast.LENGTH_SHORT).show(); + // Toast.makeText(MainActivity.this, "Failed to complete trajectory upload", Toast.LENGTH_SHORT).show(); }; + // endregion + + // File saving logic + + // Stops the sensor recording and saves the trajectory data to a file. + // The file is saved with a .protobuf extension in the app's external files directory. + // Call this method in the Stop button of HomeFragment: ((MainActivity)getActivity()).stopRecordingAndSave(); + public void stopRecordingAndSave() { + // Stop sensor collection + if (sensorFusion != null) { + sensorFusion.stopRecording(); + } + + // Prepare to save + try { + // File name suffix must be .protobuf for History interface recognition + String filename = "Traj_" + System.currentTimeMillis() + ".protobuf"; + + // Get App private storage path + File file = new File(getExternalFilesDir(null), filename); + FileOutputStream fos = new FileOutputStream(file); + + // Use writeTo to write binary stream directly + // Note: You need to add a public Traj.Trajectory.Builder getTrajectory() method in SensorFusion.java + if (sensorFusion != null && sensorFusion.getTrajectory() != null) { + sensorFusion.getTrajectory().build().writeTo(fos); + Toast.makeText(this, "File Saved: " + filename, Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(this, "Error: No data to save", Toast.LENGTH_SHORT).show(); + } + + fos.close(); + + } catch (IOException e) { + e.printStackTrace(); + Toast.makeText(this, "Save Failed: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + } + } + + // Helper method to start recording with a filename from a Fragment + public void startRecording(String filename) { + if (sensorFusion != null) { + sensorFusion.startRecording(filename); + } + } +} + - //endregion -} \ No newline at end of file diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/activity/RecordingActivity.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/activity/RecordingActivity.java index c0d82ae2..a4fe9fd3 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/activity/RecordingActivity.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/activity/RecordingActivity.java @@ -8,36 +8,13 @@ import androidx.fragment.app.FragmentTransaction; import com.openpositioning.PositionMe.R; -import com.openpositioning.PositionMe.presentation.fragment.StartLocationFragment; -import com.openpositioning.PositionMe.presentation.fragment.RecordingFragment; import com.openpositioning.PositionMe.presentation.fragment.CorrectionFragment; +import com.openpositioning.PositionMe.presentation.fragment.RecordingFragment; +import com.openpositioning.PositionMe.presentation.fragment.StartLocationFragment; - -/** - * The RecordingActivity manages the recording flow of the application, guiding the user through a sequence - * of screens for location selection, recording, and correction before finalizing the process. - *

- * This activity follows a structured workflow: - *

    - *
  1. StartLocationFragment - Allows users to select their starting location.
  2. - *
  3. RecordingFragment - Handles the recording process and contains a TrajectoryMapFragment.
  4. - *
  5. CorrectionFragment - Enables users to review and correct recorded data before completion.
  6. - *
- *

- * The activity ensures that the screen remains on during the recording process to prevent interruptions. - * It also provides fragment transactions for seamless navigation between different stages of the workflow. - *

- * This class is referenced in various fragments such as HomeFragment, StartLocationFragment, - * RecordingFragment, and CorrectionFragment to control navigation through the recording flow. - * - * @see StartLocationFragment The first step in the recording process where users select their starting location. - * @see RecordingFragment Handles data recording and map visualization. - * @see CorrectionFragment Allows users to review and make corrections before finalizing the process. - * @see com.openpositioning.PositionMe.R.layout#activity_recording The associated layout for this activity. - * - * @author ShuGu - */ - +// RecordingActivity (Updated) +// Manages the navigation flow: StartLocation -> Recording -> Correction. +// Keeps the screen on during the process. public class RecordingActivity extends AppCompatActivity { @Override @@ -46,35 +23,33 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { setContentView(R.layout.activity_recording); if (savedInstanceState == null) { - showStartLocationScreen(); // Start with the user selecting the start location + // Step 1: Start with Location Selection + showStartLocationScreen(); } - // Keep screen on + // Keep screen on to prevent interruption during sensor recording getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } - /** - * Show the StartLocationFragment (beginning of flow). - */ + // Show the StartLocationFragment. public void showStartLocationScreen() { FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); ft.replace(R.id.mainFragmentContainer, new StartLocationFragment()); ft.commit(); } - /** - * Show the RecordingFragment, which contains the TrajectoryMapFragment internally. - */ + // Show the RecordingFragment. + // Called by StartLocationFragment after location is confirmed. public void showRecordingScreen() { FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); ft.replace(R.id.mainFragmentContainer, new RecordingFragment()); + // Add to back stack so user can go back to change start location if needed ft.addToBackStack(null); ft.commit(); } - /** - * Show the CorrectionFragment after the user stops recording. - */ + // Show the CorrectionFragment. + // Should be called by RecordingFragment after recording stops. public void showCorrectionScreen() { FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); ft.replace(R.id.mainFragmentContainer, new CorrectionFragment()); @@ -82,11 +57,10 @@ public void showCorrectionScreen() { ft.commit(); } - /** - * Finish the Activity (or do any final steps) once corrections are done. - */ + // Finish the Activity flow. public void finishFlow() { getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); finish(); } } + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/activity/ReplayActivity.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/activity/ReplayActivity.java index c6a30472..060c5a88 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/activity/ReplayActivity.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/activity/ReplayActivity.java @@ -12,32 +12,28 @@ import com.openpositioning.PositionMe.presentation.fragment.StartLocationFragment; -/** - * The ReplayActivity is responsible for managing the replay session of a user's trajectory. - * It handles the process of retrieving the trajectory data, displaying relevant fragments, and - * facilitating the interaction with the user to choose the starting location before displaying the - * replay of the trajectory. - *

- * The activity starts by extracting the trajectory file path from the intent that launched it. If - * the file path is not provided or is empty, it uses a default file path. It ensures that the trajectory - * file exists before proceeding. Once the file is verified, it shows the StartLocationFragment, which allows - * the user to select their starting location (latitude and longitude). After the user has selected the - * starting point, the activity switches to the ReplayFragment to display the replay of the user's trajectory. - *

- * The activity also provides functionality to finish the replay session and exit the activity once the replay - * process has completed. - *

- * This activity makes use of a few key constants for passing data between fragments, including the trajectory file - * path and the initial latitude and longitude. These constants are defined at the beginning of the class. - *

- * The ReplayActivity manages the interaction between fragments by facilitating communication from the - * StartLocationFragment to the ReplayFragment, where the replay of the trajectory is displayed. - * - * @see StartLocationFragment The fragment where the user selects their start location for the trajectory replay. - * @see ReplayFragment The fragment responsible for showing the trajectory replay. - * - * @author Shu Gu - */ +// The ReplayActivity is responsible for managing the replay session of a user's trajectory. +// It handles the process of retrieving the trajectory data, displaying relevant fragments, and +// facilitating the interaction with the user to choose the starting location before displaying the +// replay of the trajectory. +//

+// The activity starts by extracting the trajectory file path from the intent that launched it. If +// the file path is not provided or is empty, it uses a default file path. It ensures that the trajectory +// file exists before proceeding. Once the file is verified, it shows the StartLocationFragment, which allows +// the user to select their starting location (latitude and longitude). After the user has selected the +// starting point, the activity switches to the ReplayFragment to display the replay of the user's trajectory. +//

+// The activity also provides functionality to finish the replay session and exit the activity once the replay +// process has completed. +//

+// This activity makes use of a few key constants for passing data between fragments, including the trajectory file +// path and the initial latitude and longitude. These constants are defined at the beginning of the class. +//

+// The ReplayActivity manages the interaction between fragments by facilitating communication from the +// StartLocationFragment to the ReplayFragment, where the replay of the trajectory is displayed. +// Related: StartLocationFragment The fragment where the user selects their start location for the trajectory replay. +// Related: ReplayFragment The fragment responsible for showing the trajectory replay. +// @author Shu Gu public class ReplayActivity extends AppCompatActivity { @@ -77,10 +73,8 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { } } - /** - * Display a StartLocationFragment to let user set their start location. - * Displays the ReplayFragment and passes the trajectory file path as an argument. - */ + // Display a StartLocationFragment to let user set their start location. + // Displays the ReplayFragment and passes the trajectory file path as an argument. private void showStartLocationFragment() { Log.d(TAG, "Showing StartLocationFragment..."); StartLocationFragment startLocationFragment = new StartLocationFragment(); @@ -90,17 +84,13 @@ private void showStartLocationFragment() { .commit(); } - /** - * Called by StartLocationFragment when user picks their start location. - */ + // Called by StartLocationFragment when user picks their start location. public void onStartLocationChosen(float lat, float lon) { Log.i(TAG, "User selected start location: Lat=" + lat + ", Lon=" + lon); showReplayFragment(filePath, lat, lon); } - /** - * Display ReplayFragment, passing file path and starting lat/lon as arguments. - */ + // Display ReplayFragment, passing file path and starting lat/lon as arguments. public void showReplayFragment(String filePath, float initialLat, float initialLon) { Log.d(TAG, "Switching to ReplayFragment with file: " + filePath + ", Initial Lat: " + initialLat + ", Initial Lon: " + initialLon); @@ -119,12 +109,11 @@ public void showReplayFragment(String filePath, float initialLat, float initialL .commit(); } - /** - * Finish replay session - * Called when the replay process is completed. - */ + // Finish replay session + // Called when the replay process is completed. public void finishFlow() { Log.d(TAG, "Replay session finished."); finish(); } -} \ No newline at end of file +} + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/CorrectionFragment.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/CorrectionFragment.java index 8f94cb27..8d16a0f8 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/CorrectionFragment.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/CorrectionFragment.java @@ -1,53 +1,36 @@ package com.openpositioning.PositionMe.presentation.fragment; import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; -import android.widget.EditText; -import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; -import com.openpositioning.PositionMe.R; -import com.openpositioning.PositionMe.presentation.activity.RecordingActivity; -import com.openpositioning.PositionMe.sensors.SensorFusion; -import com.openpositioning.PositionMe.utils.PathView; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.OnMapReadyCallback; import com.google.android.gms.maps.SupportMapFragment; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.MarkerOptions; +import com.openpositioning.PositionMe.R; +import com.openpositioning.PositionMe.sensors.SensorFusion; +import com.openpositioning.PositionMe.utils.IndoorMapManager; -/** - * A simple {@link Fragment} subclass. Corrections Fragment is displayed after a recording session - * is finished to enable manual adjustments to the PDR. The adjustments are not saved as of now. - */ public class CorrectionFragment extends Fragment { - //Map variable - public GoogleMap mMap; - //Button to go to next - private Button button; - //Singleton SensorFusion class + // UI components + private Button uploadButton; + + // Map and logic + private GoogleMap gMap; private SensorFusion sensorFusion = SensorFusion.getInstance(); - private TextView averageStepLengthText; - private EditText stepLengthInput; - private float averageStepLength; - private float newStepLength; - private int secondPass = 0; - private CharSequence changedText; - private static float scalingRatio = 0f; - private static LatLng start; - private PathView pathView; + private IndoorMapManager indoorMapManager; public CorrectionFragment() { // Required empty public constructor @@ -60,102 +43,55 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, if (activity != null && activity.getSupportActionBar() != null) { activity.getSupportActionBar().hide(); } - View rootView = inflater.inflate(R.layout.fragment_correction, container, false); - - // Send trajectory data to the cloud - sensorFusion.sendTrajectoryToCloud(); - - //Obtain start position - float[] startPosition = sensorFusion.getGNSSLatitude(true); - - // Initialize map fragment - SupportMapFragment supportMapFragment=(SupportMapFragment) - getChildFragmentManager().findFragmentById(R.id.map); - - supportMapFragment.getMapAsync(new OnMapReadyCallback() { - @Override - public void onMapReady(GoogleMap map) { - mMap = map; - mMap.setMapType(GoogleMap.MAP_TYPE_HYBRID); - mMap.getUiSettings().setCompassEnabled(true); - mMap.getUiSettings().setTiltGesturesEnabled(true); - mMap.getUiSettings().setRotateGesturesEnabled(true); - mMap.getUiSettings().setScrollGesturesEnabled(true); - - // Add a marker at the start position - start = new LatLng(startPosition[0], startPosition[1]); - mMap.addMarker(new MarkerOptions().position(start).title("Start Position")); - - // Calculate zoom for demonstration - double zoom = Math.log(156543.03392f * Math.cos(startPosition[0] * Math.PI / 180) - * scalingRatio) / Math.log(2); - mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(start, (float) zoom)); - } - }); - - return rootView; + return inflater.inflate(R.layout.fragment_correction, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - this.averageStepLengthText = view.findViewById(R.id.averageStepView); - this.stepLengthInput = view.findViewById(R.id.inputStepLength); - this.pathView = view.findViewById(R.id.pathView1); - - averageStepLength = sensorFusion.passAverageStepLength(); - averageStepLengthText.setText(getString(R.string.averageStepLgn) + ": " - + String.format("%.2f", averageStepLength)); - - // Listen for ENTER key - this.stepLengthInput.setOnKeyListener((v, keyCode, event) -> { - if (keyCode == KeyEvent.KEYCODE_ENTER) { - newStepLength = Float.parseFloat(changedText.toString()); - // Rescale path - sensorFusion.redrawPath(newStepLength / averageStepLength); - averageStepLengthText.setText(getString(R.string.averageStepLgn) - + ": " + String.format("%.2f", newStepLength)); - pathView.invalidate(); - - secondPass++; - if (secondPass == 2) { - averageStepLength = newStepLength; - secondPass = 0; + uploadButton = view.findViewById(R.id.uploadButton); + + // Initialize Map + SupportMapFragment mapFragment = (SupportMapFragment) getChildFragmentManager() + .findFragmentById(R.id.map); // Note: Map ID must be 'map' in XML + + if (mapFragment != null) { + mapFragment.getMapAsync(new OnMapReadyCallback() { + @Override + public void onMapReady(GoogleMap googleMap) { + gMap = googleMap; + gMap.setMapType(GoogleMap.MAP_TYPE_HYBRID); + gMap.getUiSettings().setCompassEnabled(true); + gMap.getUiSettings().setZoomControlsEnabled(true); + + indoorMapManager = new IndoorMapManager(gMap, requireContext()); + drawTrajectory(); } - } - return false; - }); - - this.stepLengthInput.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count,int after) {} - @Override - public void onTextChanged(CharSequence s, int start, int before,int count) {} - @Override - public void afterTextChanged(Editable s) { - changedText = s; - } - }); - - // Button to finalize corrections - this.button = view.findViewById(R.id.correction_done); - this.button.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - // ************* CHANGED CODE HERE ************* - // Before: - // NavDirections action = CorrectionFragmentDirections.actionCorrectionFragmentToHomeFragment(); - // Navigation.findNavController(view).navigate(action); - // ((AppCompatActivity)getActivity()).getSupportActionBar().show(); - - // Now, simply tell the Activity we are done: - ((RecordingActivity) requireActivity()).finishFlow(); - } - }); + }); + } + + // Upload Button Listener + if (uploadButton != null) { + uploadButton.setOnClickListener(v -> { + sensorFusion.sendTrajectoryToCloud(); + Toast.makeText(getContext(), "Trajectory Uploaded!", Toast.LENGTH_SHORT).show(); + }); + } } - public void setScalingRatio(float scalingRatio) { - this.scalingRatio = scalingRatio; + private void drawTrajectory() { + if (gMap == null) return; + + float[] startGen = sensorFusion.getGNSSLatitude(true); + LatLng startPos = new LatLng(startGen[0], startGen[1]); + + gMap.addMarker(new MarkerOptions().position(startPos).title("Start")); + gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(startPos, 19f)); + + if (indoorMapManager != null) { + indoorMapManager.setCurrentLocation(startPos); + } } } + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/FilesFragment.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/FilesFragment.java index 83bc4ef1..85b5e915 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/FilesFragment.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/FilesFragment.java @@ -32,17 +32,13 @@ import java.util.List; import java.util.Map; -/** - * A simple {@link Fragment} subclass. The files fragments displays a list of trajectories already - * uploaded with some metadata, and enabled re-downloading them to the device's local storage. - * - * @see HomeFragment the connected fragment in the nav graph. - * @see UploadFragment sub-menu for uploading recordings that failed during recording. - * @see com.openpositioning.PositionMe.Traj the data structure sent and received. - * @see ServerCommunications the class handling communication with the server. - * - * @author Mate Stodulka - */ +// A simple {@link Fragment} subclass. The files fragments displays a list of trajectories already +// uploaded with some metadata, and enabled re-downloading them to the device's local storage. +// Related: HomeFragment the connected fragment in the nav graph. +// Related: UploadFragment sub-menu for uploading recordings that failed during recording. +// Related: com.openpositioning.PositionMe.Traj the data structure sent and received. +// Related: ServerCommunications the class handling communication with the server. +// @author Mate Stodulka public class FilesFragment extends Fragment implements Observer { // UI elements @@ -53,18 +49,14 @@ public class FilesFragment extends Fragment implements Observer { // Class handling HTTP communication private ServerCommunications serverCommunications; - /** - * Default public constructor, empty. - */ + // Default public constructor, empty. public FilesFragment() { // Required empty public constructor } - /** - * {@inheritDoc} - * Initialise the server communication class and register the FilesFragment as an Observer to - * receive the async http responses. - */ + // {@inheritDoc} + // Initialise the server communication class and register the FilesFragment as an Observer to + // receive the async http responses. @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -72,10 +64,8 @@ public void onCreate(Bundle savedInstanceState) { serverCommunications.registerObserver(this); } - /** - * {@inheritDoc} - * Sets the title in the action bar. - */ + // {@inheritDoc} + // Sets the title in the action bar. @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -85,15 +75,12 @@ 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 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. - */ + // {@inheritDoc} + // Initialises UI elements, including a navigation card to the {@link UploadFragment} and a + // RecyclerView displaying online trajectories. +// Related: TrajDownloadViewHolder the View Holder for the list. +// Related: TrajDownloadListAdapter the list adapter for displaying the recycler view. +// Related: 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); @@ -102,10 +89,8 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat // Get clickable card view uploadCard = view.findViewById(R.id.uploadCard); uploadCard.setOnClickListener(new View.OnClickListener() { - /** - * {@inheritDoc} - * Navigates to {@link UploadFragment}. - */ + // {@inheritDoc} + // Navigates to {@link UploadFragment}. @Override public void onClick(View view) { NavDirections action = FilesFragmentDirections.actionFilesFragmentToUploadFragment(); @@ -123,13 +108,10 @@ public void onClick(View view) { }, 500); } - /** - * {@inheritDoc} - * Called by {@link ServerCommunications} when the response to the HTTP info request is received. - * - * @param singletonStringList a single string wrapped in an object array containing the http - * response from the server. - */ + // {@inheritDoc} + // Called by {@link ServerCommunications} when the response to the HTTP info request is received. +// Parameter singletonStringList: a single string wrapped in an object array containing the http + // response from the server. @Override public void update(Object[] singletonStringList) { // Cast input as a string @@ -149,14 +131,11 @@ public void run() { } } - /** - * Parses the info response string from the HTTP communication. - * Process the data using the Json library and return the matching Java data structure as a - * List of Maps of \. Throws a JSONException if the data is not valid. - * - * @param infoString HTTP info request response as a single string - * @return List of Maps of String to String containing ID, owner ID, and date. - */ + // Parses the info response string from the HTTP communication. + // Process the data using the Json library and return the matching Java data structure as a + // List of Maps of \. Throws a JSONException if the data is not valid. +// Parameter infoString: HTTP info request response as a single string +// Returns: List of Maps of String to String containing ID, owner ID, and date. private List> processInfoResponse(String infoString) { // Initialise empty list List> entryList = new ArrayList<>(); @@ -176,21 +155,29 @@ private List> processInfoResponse(String infoString) { System.err.println("JSON reading failed"); e.printStackTrace(); } - // Sort the list by the ID fields of the maps - entryList.sort(Comparator.comparing(m -> Integer.parseInt(m.get("id")), Comparator.nullsLast(Comparator.naturalOrder()))); + // Sort by numeric ID when possible; keep invalid or missing IDs at the end. + entryList.sort(Comparator.comparingInt(m -> safeParseId(m.get("id")))); return entryList; } - /** - * Update the RecyclerView in the FilesFragment with new data. - * Must be called from a UI thread. Initialises a new Layout Manager, and passes it to the - * RecyclerView. Initialises a {@link TrajDownloadListAdapter} with the input array and setting - * up a listener so that trajectories are downloaded when clicked, and a pop-up message is - * displayed to notify the user. - * - * @param entryList List of Maps of String to String containing metadata about the uploaded - * trajectories (ID, owner ID, date). - */ + private int safeParseId(String id) { + if (id == null) { + return Integer.MAX_VALUE; + } + try { + return Integer.parseInt(id); + } catch (NumberFormatException e) { + return Integer.MAX_VALUE; + } + } + + // Update the RecyclerView in the FilesFragment with new data. + // Must be called from a UI thread. Initialises a new Layout Manager, and passes it to the + // RecyclerView. Initialises a {@link TrajDownloadListAdapter} with the input array and setting + // up a listener so that trajectories are downloaded when clicked, and a pop-up message is + // displayed to notify the user. +// Parameter entryList: List of Maps of String to String containing metadata about the uploaded + // trajectories (ID, owner ID, date). private void updateView(List> entryList) { // Initialise RecyclerView with Manager and Adapter LinearLayoutManager manager = new LinearLayoutManager(getActivity()); @@ -204,18 +191,19 @@ private void updateView(List> entryList) { // Pass ID and date_submitted serverCommunications.downloadTrajectory(position, id, dateSubmitted); -// new AlertDialog.Builder(getContext()) -// .setTitle("File downloaded") -// .setMessage("Trajectory downloaded to local storage") -// .setPositiveButton(R.string.ok, null) -// .setNegativeButton(R.string.show_storage, (dialogInterface, i) -> { -// startActivity(new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)); -// }) -// .setIcon(R.drawable.ic_baseline_download_24) -// .show(); +// new AlertDialog.Builder(getContext()) +// .setTitle("File downloaded") +// .setMessage("Trajectory downloaded to local storage") +// .setPositiveButton(R.string.ok, null) +// .setNegativeButton(R.string.show_storage, (dialogInterface, i) -> { +// startActivity(new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)); +// }) +// .setIcon(R.drawable.ic_baseline_download_24) +// .show(); }); filesList.setAdapter(listAdapter); // Force refresh RecyclerView to ensure downloadRecords changes are detected listAdapter.notifyDataSetChanged(); } -} \ No newline at end of file +} + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/HomeFragment.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/HomeFragment.java index 8371b04e..cd62b752 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/HomeFragment.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/HomeFragment.java @@ -31,18 +31,14 @@ import com.openpositioning.PositionMe.R; import com.openpositioning.PositionMe.presentation.activity.RecordingActivity; -/** - * A simple {@link Fragment} subclass. The home fragment is the start screen of the application. - * The home fragment acts as a hub for all other fragments, with buttons and icons for navigation. - * The default screen when opening the application - * - * @see RecordingFragment - * @see FilesFragment - * @see MeasurementsFragment - * @see SettingsFragment - * - * @author Mate Stodulka - */ +// A simple {@link Fragment} subclass. The home fragment is the start screen of the application. +// The home fragment acts as a hub for all other fragments, with buttons and icons for navigation. +// The default screen when opening the application +// Related: RecordingFragment +// Related: FilesFragment +// Related: MeasurementsFragment +// Related: SettingsFragment +// @author Mate Stodulka public class HomeFragment extends Fragment implements OnMapReadyCallback { // Interactive UI elements to navigate to other fragments @@ -50,6 +46,7 @@ public class HomeFragment extends Fragment implements OnMapReadyCallback { private Button start; private Button measurements; private Button files; + private MaterialButton indoorButton; private TextView gnssStatusTextView; // For the map @@ -65,10 +62,8 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } - /** - * {@inheritDoc} - * Ensure the action bar is shown at the top of the screen. Set the title visible to Home. - */ + // {@inheritDoc} + // Ensure the action bar is shown at the top of the screen. Set the title visible to Home. @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -78,9 +73,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, return rootView; } - /** - * Initialise UI elements and set onClick actions for the buttons. - */ + // Initialise UI elements and set onClick actions for the buttons. @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); @@ -116,6 +109,13 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat Navigation.findNavController(v).navigate(action); }); + // Indoor Positioning button + indoorButton = view.findViewById(R.id.indoorButton); + indoorButton.setOnClickListener(v -> { + // Navigate to IndoorPositioningFragment + Navigation.findNavController(v).navigate(R.id.action_homeFragment_to_indoorPositioningFragment); + }); + // TextView to display GNSS disabled message gnssStatusTextView = view.findViewById(R.id.gnssStatusTextView); @@ -128,9 +128,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat } } - /** - * Callback triggered when the Google Map is ready to be used. - */ + // Callback triggered when the Google Map is ready to be used. @Override public void onMapReady(@NonNull GoogleMap googleMap) { mMap = googleMap; @@ -143,9 +141,7 @@ public void onResume() { checkAndUpdatePermissions(); } - /** - * Checks if GNSS/Location is enabled on the device. - */ + // Checks if GNSS/Location is enabled on the device. private boolean isGnssEnabled() { LocationManager locationManager = (LocationManager) requireContext().getSystemService(Context.LOCATION_SERVICE); @@ -155,9 +151,7 @@ private boolean isGnssEnabled() { return (gpsEnabled || networkEnabled); } - /** - * Move the map to the University of Edinburgh and display a message. - */ + // Move the map to the University of Edinburgh and display a message. private void showEdinburghAndMessage(String message) { gnssStatusTextView.setText(message); gnssStatusTextView.setVisibility(View.VISIBLE); @@ -193,19 +187,17 @@ private void checkAndUpdatePermissions() { mMap.setMyLocationEnabled(true); // Optionally move the camera to last known or default location: - // (You could retrieve it from FusedLocationProvider or similar). + // (You could retrieve it from FusedLocationProvider or similar). // Here, just leaving it on default. // If you want to center on the user as soon as it loads, do something like: - /* - FusedLocationProviderClient fusedLocationClient = - LocationServices.getFusedLocationProviderClient(requireContext()); - fusedLocationClient.getLastLocation().addOnSuccessListener(location -> { - if (location != null) { - LatLng currentLatLng = new LatLng(location.getLatitude(), location.getLongitude()); - mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(currentLatLng, 15f)); - } - }); - */ + // FusedLocationProviderClient fusedLocationClient = +// LocationServices.getFusedLocationProviderClient(requireContext()); +// fusedLocationClient.getLastLocation().addOnSuccessListener(location -> { +// if (location != null) { +// LatLng currentLatLng = new LatLng(location.getLatitude(), location.getLongitude()); +// mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(currentLatLng, 15f)); +// } +// }); } else { // If no permission, simply show a default location or prompt for permissions showEdinburghAndMessage("Permission not granted. Please enable in settings."); @@ -216,3 +208,5 @@ private void checkAndUpdatePermissions() { } } } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/IndoorMapFragment.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/IndoorMapFragment.java index 48c40474..bf1125e7 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/IndoorMapFragment.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/IndoorMapFragment.java @@ -1,24 +1,229 @@ package com.openpositioning.PositionMe.presentation.fragment; +import android.util.Log; + import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.model.BitmapDescriptorFactory; import com.google.android.gms.maps.model.BitmapDescriptor; - import com.google.android.gms.maps.model.GroundOverlay; import com.google.android.gms.maps.model.GroundOverlayOptions; +import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; +import com.google.android.gms.maps.model.Polygon; +import com.google.android.gms.maps.model.PolygonOptions; +import com.openpositioning.PositionMe.data.remote.IndoorMapAPI; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +// IndoorMapFragment - Enhanced version with API support +// Handles: +// Fetching building and floor data from API +// Drawing building outlines on map +// Displaying indoor floor plans +// Managing venue/building selection public class IndoorMapFragment { + private static final String TAG = "IndoorMapFragment"; + private GoogleMap mMap; private GroundOverlay[] groundOverlays; // GroundOverlay used to store each layer private int currentFloor = 0; // Floor by default + private int floorCount = 0; + + // Indoor map API + private IndoorMapAPI indoorMapAPI; + + // Building data + private Map buildingMap = new HashMap<>(); + private Map floorMap = new HashMap<>(); + + // Map overlays + private List buildingPolygons = new ArrayList<>(); + private String currentBuildingId = null; + private String currentVenueName = null; + + // Callback for venue selection + private VenueSelectionCallback venueCallback; + + public interface VenueSelectionCallback { + void onVenueSelected(String buildingId, String venueName); + } + + public IndoorMapFragment() { + this.indoorMapAPI = new IndoorMapAPI(); + } public IndoorMapFragment(GoogleMap map, int floorNumber) { this.mMap = map; // Pass in Google Maps + this.floorCount = floorNumber; this.groundOverlays = new GroundOverlay[floorNumber]; // Set the number of floors + this.indoorMapAPI = new IndoorMapAPI(); + } + + // Set the venue selection callback + public void setVenueSelectionCallback(VenueSelectionCallback callback) { + this.venueCallback = callback; + } + + // Fetch buildings near user location + public void fetchNearbyBuildings(double latitude, double longitude, double radiusMeters) { + if (indoorMapAPI == null) return; + + indoorMapAPI.fetchNearbyBuildings(latitude, longitude, radiusMeters, + new IndoorMapAPI.BuildingsCallback() { + @Override + public void onSuccess(List buildings) { + Log.d(TAG, "Received " + buildings.size() + " buildings"); + displayBuildings(buildings); + } + + @Override + public void onError(String error) { + Log.e(TAG, "Error fetching buildings: " + error); + } + }); + } + + // Display building outlines and enable selection + private void displayBuildings(List buildings) { + if (mMap == null) return; + + // Clear previous polygons + for (Polygon polygon : buildingPolygons) { + polygon.remove(); + } + buildingPolygons.clear(); + buildingMap.clear(); + + // Add each building as a marker and polygon + for (final IndoorMapAPI.BuildingInfo building : buildings) { + buildingMap.put(building.buildingId, building); + + // Add building name as marker + com.google.android.gms.maps.model.MarkerOptions markerOptions = + new com.google.android.gms.maps.model.MarkerOptions() + .position(new LatLng(building.latitude, building.longitude)) + .title(building.buildingName) + .snippet("Taps to select"); + + final com.google.android.gms.maps.model.Marker marker = mMap.addMarker(markerOptions); + + // Fetch and display building outline + indoorMapAPI.fetchBuildingOutline(building.buildingId, + new IndoorMapAPI.OutlineCallback() { + @Override + public void onSuccess(double[][] coordinates) { + if (coordinates.length > 0) { + drawBuildingOutline(building.buildingId, building.buildingName, coordinates); + } + } + + @Override + public void onError(String error) { + Log.w(TAG, "Could not fetch outline for building " + building.buildingId); + } + }); + } + + // Set up marker click listener to select venue + mMap.setOnMarkerClickListener(marker -> { + String buildingId = null; + for (IndoorMapAPI.BuildingInfo b : buildingMap.values()) { + if (new LatLng(b.latitude, b.longitude).equals(marker.getPosition())) { + buildingId = b.buildingId; + break; + } + } + + if (buildingId != null) { + selectVenue(buildingId, marker.getTitle()); + // Fetch and display floors for this building + fetchBuildingFloors(buildingId); + } + return true; + }); + } + + // Draw building outline as polygon + private void drawBuildingOutline(String buildingId, String buildingName, double[][] coordinates) { + if (mMap == null || coordinates.length == 0) return; + + PolygonOptions polygonOptions = new PolygonOptions() + .strokeColor(0xFF0000FF) // Blue + .fillColor(0x220000FF) // Semi-transparent blue + .strokeWidth(3.0f); + + for (double[] coord : coordinates) { + polygonOptions.add(new LatLng(coord[0], coord[1])); + } + + Polygon polygon = mMap.addPolygon(polygonOptions); + buildingPolygons.add(polygon); + + // Store building ID in polygon tag for later reference + polygon.setTag(buildingId); } + + // Fetch and display floor plans for selected building + public void fetchBuildingFloors(String buildingId) { + indoorMapAPI.fetchBuildingFloors(buildingId, + new IndoorMapAPI.FloorsCallback() { + @Override + public void onSuccess(List floors) { + Log.d(TAG, "Received " + floors.size() + " floors"); + displayFloors(floors); + } - // Used to add floors + @Override + public void onError(String error) { + Log.e(TAG, "Error fetching floors: " + error); + } + }); + } + + // Display floor plans as ground overlays + private void displayFloors(List floors) { + if (mMap == null || floors.isEmpty()) return; + + // Clear existing overlays + if (groundOverlays != null) { + for (GroundOverlay overlay : groundOverlays) { + if (overlay != null) { + overlay.remove(); + } + } + } + + // Create new array for floors + groundOverlays = new GroundOverlay[floors.size()]; + floorCount = floors.size(); + + for (int i = 0; i < floors.size(); i++) { + IndoorMapAPI.FloorPlan floor = floors.get(i); + LatLngBounds bounds = new LatLngBounds( + new LatLng(floor.minLat, floor.minLon), + new LatLng(floor.maxLat, floor.maxLon) + ); + + // Add floor from URL if available + if (!floor.imageUrl.isEmpty()) { + addFloorFromUrl(i, floor.imageUrl, bounds); + } + + floorMap.put(floor.floorId, floor); + Log.d(TAG, "Added floor: " + floor.floorName); + } + + // Show first floor by default + currentFloor = 0; + if (groundOverlays.length > 0 && groundOverlays[0] != null) { + groundOverlays[0].setVisible(true); + } + } + + // Used to add floors from drawable resource public void addFloor(int floorIndex, int drawableResId, LatLngBounds bounds) { BitmapDescriptor image = BitmapDescriptorFactory.fromResource(drawableResId); GroundOverlayOptions groundOverlayOptions = new GroundOverlayOptions() @@ -27,12 +232,32 @@ public void addFloor(int floorIndex, int drawableResId, LatLngBounds bounds) { .visible(floorIndex == currentFloor) .transparency(0.2f); - groundOverlays[floorIndex] = mMap.addGroundOverlay(groundOverlayOptions); + if (groundOverlays != null && floorIndex < groundOverlays.length) { + groundOverlays[floorIndex] = mMap.addGroundOverlay(groundOverlayOptions); + } + } + + // Add floor plan from URL (as bitmap) + public void addFloorFromUrl(int floorIndex, String imageUrl, LatLngBounds bounds) { + // For now, we'll use a placeholder + // In production, you'd need to download the image from URL + // and cache it locally + BitmapDescriptor image = BitmapDescriptorFactory.defaultMarker(); + + GroundOverlayOptions groundOverlayOptions = new GroundOverlayOptions() + .image(image) + .positionFromBounds(bounds) + .visible(false) // Hidden initially + .transparency(0.2f); + + if (groundOverlays != null && floorIndex < groundOverlays.length) { + groundOverlays[floorIndex] = mMap.addGroundOverlay(groundOverlayOptions); + } } // Switch floors and make sure only one floor is displayed public void switchFloor(int floorIndex) { - if (floorIndex < 0 || floorIndex >= groundOverlays.length) { + if (groundOverlays == null || floorIndex < 0 || floorIndex >= groundOverlays.length) { return; // Prevent index out of bounds } // Hide all floors @@ -47,15 +272,41 @@ public void switchFloor(int floorIndex) { selectedOverlay.setVisible(true); } currentFloor = floorIndex; + Log.d(TAG, "Switched to floor " + floorIndex); } // Hide all floors public void hideMap() { - //Hide all floors + if (groundOverlays == null) return; + // Hide all floors for (GroundOverlay overlay : groundOverlays) { if (overlay != null) { overlay.setVisible(false); } } } + + // Select a venue/building and notify callback + private void selectVenue(String buildingId, String venueName) { + this.currentBuildingId = buildingId; + this.currentVenueName = venueName; + + Log.d(TAG, "Venue selected: " + venueName + " (" + buildingId + ")"); + + if (venueCallback != null) { + venueCallback.onVenueSelected(buildingId, venueName); + } + } + + // Get selected venue name + public String getSelectedVenueName() { + return currentVenueName; + } + + // Get selected building ID + public String getSelectedBuildingId() { + return currentBuildingId; + } } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/IndoorPositioningFragment.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/IndoorPositioningFragment.java new file mode 100644 index 00000000..d85f88f0 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/IndoorPositioningFragment.java @@ -0,0 +1,582 @@ +package com.openpositioning.PositionMe.presentation.fragment; + +import android.Manifest; +import android.content.pm.PackageManager; +import android.content.res.ColorStateList; +import android.location.Location; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; + +import com.google.android.gms.location.FusedLocationProviderClient; +import com.google.android.gms.location.LocationCallback; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.LocationResult; +import com.google.android.gms.location.LocationServices; +import com.google.android.gms.location.Priority; +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.material.button.MaterialButton; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.openpositioning.PositionMe.R; +import com.openpositioning.PositionMe.sensors.SensorFusion; +import com.openpositioning.PositionMe.utils.IndoorMapManager; + +import java.util.ArrayList; +import java.util.Locale; + +public class IndoorPositioningFragment extends Fragment implements OnMapReadyCallback { + private static final int AUTO_FLOOR_CONFIRMATIONS = 4; + private static final long AUTO_FLOOR_SWITCH_COOLDOWN_MS = 6000L; + + private TextView latitudeText; + private TextView longitudeText; + private TextView altitudeText; + private TextView accuracyText; + private TextView floorText; + private TextView currentFloorText; + private MaterialButton nucleusButton; + private MaterialButton libraryButton; + private MaterialButton murchisonButton; + private MaterialButton fjbButton; + private MaterialButton locationToggleButton; + private MaterialButton buildingToggleButton; + private FloatingActionButton floorUpBtn; + private FloatingActionButton floorDownBtn; + private View floorControlsLayout; + private View locationContent; + private View buildingContent; + + private GoogleMap googleMap; + private IndoorMapManager indoorMapManager; + private FusedLocationProviderClient fusedLocationClient; + private LocationCallback locationCallback; + private Marker currentLocationMarker; + + private SensorFusion sensorFusion; + private Double latestGpsAltitudeMeters; + private Float latestEstimatedAbsoluteAltitudeMeters; + private LatLng latestGnssLatLng; + private float latestGnssAccuracy = Float.MAX_VALUE; + private String selectedBuildingName; + private boolean locationPanelExpanded = true; + private boolean buildingPanelExpanded = true; + private int pendingAutoFloorCandidate = Integer.MIN_VALUE; + private int pendingAutoFloorCandidateCount = 0; + private long lastAutoFloorSwitchTimestampMs = 0L; + private boolean wasNearVerticalTransition = false; + + private final Handler updateHandler = new Handler(Looper.getMainLooper()); + private Runnable updateRunnable; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + AppCompatActivity activity = (AppCompatActivity) getActivity(); + if (activity != null && activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setTitle("Indoor Positioning"); + } + return inflater.inflate(R.layout.fragment_indoor_positioning, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + latitudeText = view.findViewById(R.id.latitudeText); + longitudeText = view.findViewById(R.id.longitudeText); + altitudeText = view.findViewById(R.id.altitudeText); + accuracyText = view.findViewById(R.id.accuracyText); + floorText = view.findViewById(R.id.floorText); + currentFloorText = view.findViewById(R.id.currentFloorText); + floorControlsLayout = view.findViewById(R.id.floorControlsLayout); + + nucleusButton = view.findViewById(R.id.nucleusButton); + libraryButton = view.findViewById(R.id.libraryButton); + murchisonButton = view.findViewById(R.id.murchisonButton); + fjbButton = view.findViewById(R.id.fjbButton); + locationToggleButton = view.findViewById(R.id.locationToggleButton); + buildingToggleButton = view.findViewById(R.id.buildingToggleButton); + floorUpBtn = view.findViewById(R.id.floorUpBtn); + floorDownBtn = view.findViewById(R.id.floorDownBtn); + locationContent = view.findViewById(R.id.locationContent); + buildingContent = view.findViewById(R.id.buildingContent); + + fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireContext()); + sensorFusion = SensorFusion.getInstance(); + + nucleusButton.setOnClickListener(v -> selectBuilding("Nucleus", nucleusButton)); + libraryButton.setOnClickListener(v -> selectBuilding("Library", libraryButton)); + murchisonButton.setOnClickListener(v -> selectBuilding("Murchison", murchisonButton)); + fjbButton.setOnClickListener(v -> selectBuilding("FJB", fjbButton)); + locationToggleButton.setOnClickListener(v -> toggleLocationPanel()); + buildingToggleButton.setOnClickListener(v -> toggleBuildingPanel()); + + floorUpBtn.setOnClickListener(v -> { + if (indoorMapManager != null) { + indoorMapManager.increaseFloor(); + syncIndoorFloorReference(); + updateFloorDisplay(); + } + }); + + floorDownBtn.setOnClickListener(v -> { + if (indoorMapManager != null) { + indoorMapManager.decreaseFloor(); + syncIndoorFloorReference(); + updateFloorDisplay(); + } + }); + + SupportMapFragment mapFragment = (SupportMapFragment) getChildFragmentManager() + .findFragmentById(R.id.indoorMapFragment); + if (mapFragment != null) { + mapFragment.getMapAsync(this); + } + + resetBuildingButtons(); + startPeriodicUpdates(); + requestImmediateLocationSeed(); + updateAltitudeDisplay(); + applyPanelState(locationContent, locationToggleButton, locationPanelExpanded); + applyPanelState(buildingContent, buildingToggleButton, buildingPanelExpanded); + } + + @Override + public void onMapReady(@NonNull GoogleMap map) { + googleMap = map; + googleMap.setMapType(GoogleMap.MAP_TYPE_HYBRID); + googleMap.getUiSettings().setCompassEnabled(true); + googleMap.getUiSettings().setZoomControlsEnabled(true); + + indoorMapManager = new IndoorMapManager(googleMap, requireContext()); + indoorMapManager.addFallbackBuildings(); + indoorMapManager.setOnFloorDataLoadedListener(hasData -> { + if (getActivity() != null) { + getActivity().runOnUiThread(() -> { + selectedBuildingName = indoorMapManager.getSelectedBuildingName(); + floorControlsLayout.setVisibility(hasData ? View.VISIBLE : View.GONE); + syncIndoorFloorReference(); + updateFloorDisplay(); + updatePositionDisplay(); + }); + } + }); + + googleMap.setOnPolygonClickListener(polygon -> { + if (indoorMapManager != null) { + indoorMapManager.onPolygonClick(polygon); + selectedBuildingName = indoorMapManager.getSelectedBuildingName(); + updateFloorDisplay(); + updatePositionDisplay(); + } + }); + + LatLng kbCampus = new LatLng(55.9230, -3.1750); + googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(kbCampus, 17f)); + startLocationUpdates(); + } + + private void selectBuilding(String buildingName, MaterialButton button) { + resetBuildingButtons(); + setBuildingButtonSelected(button, true); + selectedBuildingName = buildingName; + clearPendingAutoFloorCandidate(); + + LatLng buildingCenter = getBuildingCenter(buildingName); + if (buildingCenter != null && googleMap != null) { + googleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(buildingCenter, 19f)); + if (indoorMapManager != null) { + indoorMapManager.setIndoorMapVisible(true); + indoorMapManager.setSelectedBuilding(buildingName, buildingCenter); + indoorMapManager.fetchFloorPlan(buildingCenter, new ArrayList<>()); + updateFloorDisplay(); + } + } + + buildingPanelExpanded = false; + applyPanelState(buildingContent, buildingToggleButton, buildingPanelExpanded); + Toast.makeText(getContext(), "Loading " + buildingName + " indoor map...", Toast.LENGTH_SHORT).show(); + } + + private void resetBuildingButtons() { + setBuildingButtonSelected(nucleusButton, false); + setBuildingButtonSelected(libraryButton, false); + setBuildingButtonSelected(murchisonButton, false); + setBuildingButtonSelected(fjbButton, false); + } + + private void setBuildingButtonSelected(MaterialButton button, boolean selected) { + if (button == null || getContext() == null) { + return; + } + + int background = ContextCompat.getColor(requireContext(), selected ? R.color.ios_blue : R.color.ios_surface); + int text = ContextCompat.getColor(requireContext(), selected ? R.color.white : R.color.ios_label); + int stroke = ContextCompat.getColor(requireContext(), selected ? R.color.ios_blue : R.color.ios_separator); + int icon = ContextCompat.getColor(requireContext(), selected ? R.color.white : R.color.ios_blue); + + button.setBackgroundTintList(ColorStateList.valueOf(background)); + button.setTextColor(text); + button.setStrokeColor(ColorStateList.valueOf(stroke)); + button.setIconTint(ColorStateList.valueOf(icon)); + } + + private LatLng getBuildingCenter(String name) { + switch (name) { + case "Nucleus": + return new LatLng(55.92307, -3.17424); + case "Library": + return new LatLng(55.92294, -3.17497); + case "Murchison": + return new LatLng(55.92413, -3.17916); + case "FJB": + return new LatLng(55.92246, -3.17243); + default: + return null; + } + } + + private void startLocationUpdates() { + if (ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + return; + } + + requestImmediateLocationSeed(); + + LocationRequest locationRequest = new LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000) + .setMinUpdateIntervalMillis(500) + .setMinUpdateDistanceMeters(0.5f) + .build(); + + locationCallback = new LocationCallback() { + @Override + public void onLocationResult(@NonNull LocationResult locationResult) { + Location location = locationResult.getLastLocation(); + if (location != null) { + latestGnssLatLng = new LatLng(location.getLatitude(), location.getLongitude()); + if (location.hasAltitude()) { + latestGpsAltitudeMeters = location.getAltitude(); + } + if (location.hasAccuracy()) { + latestGnssAccuracy = location.getAccuracy(); + } + updatePositionDisplay(); + updateAltitudeDisplay(); + } + } + }; + + fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper()); + } + + private void requestImmediateLocationSeed() { + if (fusedLocationClient == null || ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + return; + } + + fusedLocationClient.getLastLocation().addOnSuccessListener(location -> { + if (location == null) { + return; + } + + latestGnssLatLng = new LatLng(location.getLatitude(), location.getLongitude()); + if (location.hasAltitude()) { + latestGpsAltitudeMeters = location.getAltitude(); + } + if (location.hasAccuracy()) { + latestGnssAccuracy = location.getAccuracy(); + } + + updatePositionDisplay(); + updateAltitudeDisplay(); + }); + } + + private void startPeriodicUpdates() { + updateRunnable = new Runnable() { + @Override + public void run() { + updateSensorData(); + updateHandler.postDelayed(this, 1000); + } + }; + updateHandler.post(updateRunnable); + } + + private void updateSensorData() { + if (sensorFusion != null) { + float estimatedAltitude = sensorFusion.getEstimatedAbsoluteAltitude(); + if (!Float.isNaN(estimatedAltitude)) { + latestEstimatedAbsoluteAltitudeMeters = estimatedAltitude; + } + + if (indoorMapManager != null && indoorMapManager.getAvailableFloorsCount() > 0) { + syncIndoorMapFloorWithEstimate(); + String floorName = indoorMapManager.getCurrentFloorName(); + if (floorName != null) { + floorText.setText(floorName); + } + } else { + floorText.setText(formatEstimatedFloor(sensorFusion.getEstimatedFloor())); + } + } + + updatePositionDisplay(); + updateAltitudeDisplay(); + } + + private void syncIndoorFloorReference() { + if (sensorFusion == null || indoorMapManager == null) { + return; + } + + sensorFusion.setIndoorFloorReference( + indoorMapManager.getFloorHeight(), + indoorMapManager.getCurrentFloor(), + indoorMapManager.getFloorAltitudeAnchors() + ); + sensorFusion.setIndoorEnvironmentFeatures( + indoorMapManager.getCurrentFloorStairsZones(), + indoorMapManager.getCurrentFloorLiftZones(), + indoorMapManager.getCurrentFloorWalls() + ); + } + + private void syncIndoorMapFloorWithEstimate() { + if (indoorMapManager == null || sensorFusion == null) { + return; + } + + int totalFloors = indoorMapManager.getAvailableFloorsCount(); + if (totalFloors <= 0) { + clearPendingAutoFloorCandidate(); + return; + } + + int estimatedFloor = indoorMapManager.mapBarometerBandFloorToFloorIndex( + sensorFusion.getEstimatedFloor() + ); + if (estimatedFloor == indoorMapManager.getCurrentFloor()) { + clearPendingAutoFloorCandidate(); + return; + } + + if (pendingAutoFloorCandidate == estimatedFloor) { + pendingAutoFloorCandidateCount++; + } else { + pendingAutoFloorCandidate = estimatedFloor; + pendingAutoFloorCandidateCount = 1; + } + + if (pendingAutoFloorCandidateCount >= AUTO_FLOOR_CONFIRMATIONS) { + long now = android.os.SystemClock.elapsedRealtime(); + if (now - lastAutoFloorSwitchTimestampMs < AUTO_FLOOR_SWITCH_COOLDOWN_MS) { + return; + } + indoorMapManager.setCurrentFloor(estimatedFloor, true); + syncIndoorFloorReference(); + lastAutoFloorSwitchTimestampMs = now; + clearPendingAutoFloorCandidate(); + updateFloorDisplay(); + } + } + + private void clearPendingAutoFloorCandidate() { + pendingAutoFloorCandidate = Integer.MIN_VALUE; + pendingAutoFloorCandidateCount = 0; + } + + private void updatePositionDisplay() { + LatLng displayLatLng = latestGnssLatLng; + boolean usingFusion = false; + + if (sensorFusion != null && sensorFusion.getFusedLatLng() != null) { + displayLatLng = sensorFusion.getFusedLatLng(); + usingFusion = true; + } + + if (displayLatLng == null && sensorFusion != null) { + float[] gnss = sensorFusion.getGNSSLatitude(false); + if (isValidCoordinate(gnss[0], gnss[1])) { + displayLatLng = new LatLng(gnss[0], gnss[1]); + } + } + + if (displayLatLng == null) { + latitudeText.setText("--"); + longitudeText.setText("--"); + accuracyText.setText("Waiting for location"); + return; + } + + updateBarometerAutoFloorGate(displayLatLng); + + if (indoorMapManager != null && currentLocationMarker != null) { + displayLatLng = indoorMapManager.validatePosition(displayLatLng, currentLocationMarker.getPosition()); + } + + latitudeText.setText(String.format(Locale.US, "%.6f", displayLatLng.latitude)); + longitudeText.setText(String.format(Locale.US, "%.6f", displayLatLng.longitude)); + + if (googleMap != null) { + if (currentLocationMarker == null) { + currentLocationMarker = googleMap.addMarker(new MarkerOptions() + .position(displayLatLng) + .title("Current Location")); + } else { + currentLocationMarker.setPosition(displayLatLng); + } + } + + if (usingFusion) { + float gnssAccuracy = sensorFusion != null ? sensorFusion.getGnssAccuracy() : latestGnssAccuracy; + if (gnssAccuracy < Float.MAX_VALUE) { + accuracyText.setText(String.format(Locale.US, "Fusion display | GNSS ref +/- %.1f m", gnssAccuracy)); + } else { + accuracyText.setText("Fusion display"); + } + } else if (latestGnssAccuracy < Float.MAX_VALUE) { + accuracyText.setText(String.format(Locale.US, "GNSS +/- %.1f m", latestGnssAccuracy)); + } else { + accuracyText.setText("Waiting for location"); + } + } + + private boolean isValidCoordinate(float lat, float lon) { + return lat >= -90f && lat <= 90f && lon >= -180f && lon <= 180f + && !(Math.abs(lat) < 0.00001f && Math.abs(lon) < 0.00001f); + } + + private void updateBarometerAutoFloorGate(@NonNull LatLng location) { + if (sensorFusion == null || indoorMapManager == null || indoorMapManager.getAvailableFloorsCount() <= 0) { + wasNearVerticalTransition = false; + if (sensorFusion != null) { + sensorFusion.setBarometerAutoFloorEnabled(false); + } + return; + } + + boolean nearVerticalTransition = indoorMapManager.isNearCurrentFloorVerticalTransition(location); + if (nearVerticalTransition && !wasNearVerticalTransition) { + sensorFusion.setBarometerAutoFloorEnabled(true); + } else if (!nearVerticalTransition && wasNearVerticalTransition) { + sensorFusion.setBarometerAutoFloorEnabled(false); + clearPendingAutoFloorCandidate(); + } + + wasNearVerticalTransition = nearVerticalTransition; + } + + private void updateAltitudeDisplay() { + if (altitudeText == null) { + return; + } + + if (latestEstimatedAbsoluteAltitudeMeters != null) { + altitudeText.setText(String.format(Locale.US, "%.1f m", latestEstimatedAbsoluteAltitudeMeters)); + } else if (latestGpsAltitudeMeters != null) { + altitudeText.setText(String.format(Locale.US, "%.1f m", latestGpsAltitudeMeters)); + } else { + altitudeText.setText("--"); + } + } + + private void updateFloorDisplay() { + if (indoorMapManager != null) { + int currentFloor = indoorMapManager.getCurrentFloor(); + int totalFloors = indoorMapManager.getAvailableFloorsCount(); + String floorName = indoorMapManager.getCurrentFloorName(); + + if (floorName != null && totalFloors > 0) { + currentFloorText.setText(floorName + " (" + (currentFloor + 1) + "/" + totalFloors + ")"); + floorText.setText(floorName); + } else if (selectedBuildingName != null) { + currentFloorText.setText(selectedBuildingName + " loading..."); + } else if (sensorFusion != null) { + currentFloorText.setText("Estimated " + formatEstimatedFloor(sensorFusion.getEstimatedFloor())); + } else { + currentFloorText.setText("Select a building"); + } + } + } + + private void toggleLocationPanel() { + locationPanelExpanded = !locationPanelExpanded; + applyPanelState(locationContent, locationToggleButton, locationPanelExpanded); + } + + private void toggleBuildingPanel() { + buildingPanelExpanded = !buildingPanelExpanded; + applyPanelState(buildingContent, buildingToggleButton, buildingPanelExpanded); + } + + private void applyPanelState(View content, MaterialButton toggle, boolean expanded) { + if (content != null) { + content.setVisibility(expanded ? View.VISIBLE : View.GONE); + } + if (toggle != null) { + toggle.setIconResource(expanded + ? android.R.drawable.arrow_up_float + : android.R.drawable.arrow_down_float); + } + } + + private String formatEstimatedFloor(int estimatedFloor) { + if (estimatedFloor == 0) { + return "Ground"; + } + if (estimatedFloor > 0) { + return "Floor " + estimatedFloor; + } + return "Basement " + Math.abs(estimatedFloor); + } + + @Override + public void onPause() { + super.onPause(); + if (fusedLocationClient != null && locationCallback != null) { + fusedLocationClient.removeLocationUpdates(locationCallback); + } + if (updateHandler != null && updateRunnable != null) { + updateHandler.removeCallbacks(updateRunnable); + } + } + + @Override + public void onResume() { + super.onResume(); + startLocationUpdates(); + if (updateHandler != null && updateRunnable != null) { + updateHandler.post(updateRunnable); + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + if (updateHandler != null && updateRunnable != null) { + updateHandler.removeCallbacks(updateRunnable); + } + } +} + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/InfoFragment.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/InfoFragment.java index f0cc78de..bfd59133 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/InfoFragment.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/InfoFragment.java @@ -17,18 +17,15 @@ import com.openpositioning.PositionMe.sensors.SensorInfo; import com.openpositioning.PositionMe.presentation.viewitems.SensorInfoListAdapter; +import java.util.ArrayList; import java.util.List; -/** - * A simple {@link Fragment} subclass. The info fragment display the available sensors and data - * collection devices with relevant information about their capabilities. - * - * @see HomeFragment the previous fragment in the nav graph. - * @see com.openpositioning.PositionMe.sensors.SensorFusion the class containing all sensors. - * @see SensorInfo the class used for each sensor instance's metadata - * - * @author Mate Stodulka - */ +// A simple {@link Fragment} subclass. The info fragment display the available sensors and data +// collection devices with relevant information about their capabilities. +// Related: HomeFragment the previous fragment in the nav graph. +// Related: com.openpositioning.PositionMe.sensors.SensorFusion the class containing all sensors. +// Related: SensorInfo the class used for each sensor instance's metadata +// @author Mate Stodulka public class InfoFragment extends Fragment { // Singleton SensorFusion instance to access the sensors used @@ -36,25 +33,19 @@ public class InfoFragment extends Fragment { // UI element recyclerview to display sensor information private RecyclerView sensorInfoView; - /** - * Public default constructor, empty. - */ + // Public default constructor, empty. public InfoFragment() { // Required empty public constructor } - /** - * {@inheritDoc} - */ + // {@inheritDoc} @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } - /** - * {@inheritDoc} - * Set title in the action bar to Sensor Information. - */ + // {@inheritDoc} + // Set title in the action bar to Sensor Information. @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -64,16 +55,13 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, return rootView; } - /** - * {@inheritDoc} - * Initialise the RecyclerView by creating and registering a Layout Manager, getting the - * {@link SensorFusion} instance and obtaining the Sensor Info data, and passing it to the - * {@link SensorInfoListAdapter}. - * - * @see SensorInfoListAdapter List adapter for the Sensor Info Recycler View. - * @see SensorInfoViewHolder View holder for the Sensor Infor RV. - * @see com.openpositioning.PositionMe.R.layout#item_sensorinfo_card_view - */ + // {@inheritDoc} + // Initialise the RecyclerView by creating and registering a Layout Manager, getting the + // {@link SensorFusion} instance and obtaining the Sensor Info data, and passing it to the + // {@link SensorInfoListAdapter}. +// Related: SensorInfoListAdapter List adapter for the Sensor Info Recycler View. +// Related: SensorInfoViewHolder View holder for the Sensor Infor RV. +// Related: com.openpositioning.PositionMe.R.layout#item_sensorinfo_card_view @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); @@ -81,10 +69,24 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat sensorInfoView = (RecyclerView) getView().findViewById(R.id.sensorInfoList); // Register layout manager sensorInfoView.setLayoutManager(new LinearLayoutManager(getActivity())); - // Get singleton sensor fusion instance, load sensor info data + + // Get singleton sensor fusion instance sensorFusion = SensorFusion.getInstance(); - List sensorInfoList = sensorFusion.getSensorInfos(); + + // Convert raw sensor objects to SensorInfo entries for the adapter. + List rawList = sensorFusion.getSensorInfos(); + List sensorInfoList = new ArrayList<>(); + + if (rawList != null) { + for (Object obj : rawList) { + if (obj instanceof SensorInfo) { + sensorInfoList.add((SensorInfo) obj); + } + } + } + // Set adapter for the recycler view. sensorInfoView.setAdapter(new SensorInfoListAdapter(getActivity(), sensorInfoList)); } -} \ No newline at end of file +} + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/MeasurementsFragment.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/MeasurementsFragment.java index 20c43987..c3d43fb6 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/MeasurementsFragment.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/MeasurementsFragment.java @@ -19,21 +19,21 @@ import com.openpositioning.PositionMe.sensors.SensorFusion; import com.openpositioning.PositionMe.sensors.SensorTypes; import com.openpositioning.PositionMe.sensors.Wifi; +import com.openpositioning.PositionMe.sensors.BleDevice; import com.openpositioning.PositionMe.presentation.viewitems.WifiListAdapter; +import com.openpositioning.PositionMe.presentation.viewitems.BleListAdapter; +import java.util.ArrayList; +import java.util.EnumMap; import java.util.List; import java.util.Map; -/** - * A simple {@link Fragment} subclass. The measurement fragment displays the set of current sensor - * readings. The values are refreshed periodically, but slower than their internal refresh rate. - * The refresh time is set by a static constant. - * - * @see HomeFragment the previous fragment in the nav graph. - * @see SensorFusion the source of all sensor readings. - * - * @author Mate Stodulka - */ +// A simple {@link Fragment} subclass. The measurement fragment displays the set of current sensor +// readings. The values are refreshed periodically, but slower than their internal refresh rate. +// The refresh time is set by a static constant. +// Related: HomeFragment the previous fragment in the nav graph. +// Related: SensorFusion the source of all sensor readings. +// @author Mate Stodulka public class MeasurementsFragment extends Fragment { // Static constant for refresh time in milliseconds @@ -47,25 +47,22 @@ public class MeasurementsFragment extends Fragment { // UI elements private ConstraintLayout sensorMeasurementList; private RecyclerView wifiListView; + private RecyclerView bleListView; // List of string resource IDs private int[] prefaces; private int[] gnssPrefaces; + private final Map sensorRowIds = new EnumMap<>(SensorTypes.class); - /** - * Public default constructor, empty. - */ + // Public default constructor, empty. public MeasurementsFragment() { // Required empty public constructor } - /** - * {@inheritDoc} - * Obtains the singleton Sensor Fusion instance and initialises the string prefaces for display. - * Creates a new handler to periodically refresh data. - * - * @see SensorFusion handles all sensor data. - */ + // {@inheritDoc} + // Obtains the singleton Sensor Fusion instance and initialises the string prefaces for display. + // Creates a new handler to periodically refresh data. +// Related: SensorFusion handles all sensor data. @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -74,16 +71,15 @@ public void onCreate(Bundle savedInstanceState) { // Initialise string prefaces for display prefaces = new int[]{R.string.x, R.string.y, R.string.z}; gnssPrefaces = new int[]{R.string.lati, R.string.longi}; + initialiseSensorRowIds(); // Create new handler to refresh the UI. this.refreshDataHandler = new Handler(); } - /** - * {@inheritDoc} - * Sets title in the action bar to Sensor Measurements. - * Posts the {@link MeasurementsFragment#refreshTableTask} using the Handler. - */ + // {@inheritDoc} + // Sets title in the action bar to Sensor Measurements. + // Posts the {@link MeasurementsFragment#refreshTableTask} using the Handler. @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -94,84 +90,141 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, return rootView; } - /** - * {@inheritDoc} - * Pauses the data refreshing when the fragment is not in focus. - */ + // {@inheritDoc} + // Pauses the data refreshing when the fragment is not in focus. @Override public void onPause() { refreshDataHandler.removeCallbacks(refreshTableTask); super.onPause(); } - /** - * {@inheritDoc} - * Restarts the data refresh when the fragment returns to focus. - */ + // {@inheritDoc} + // Restarts the data refresh when the fragment returns to focus. @Override public void onResume() { - refreshDataHandler.postDelayed(refreshTableTask, REFRESH_TIME); super.onResume(); + // Ensure sensors and WiFi scanner are running + sensorFusion.resumeListening(); + refreshDataHandler.postDelayed(refreshTableTask, REFRESH_TIME); } - /** - * {@inheritDoc} - * Obtains the constraint layout holding the sensor measurement values. Initialises the Recycler - * View for holding WiFi data and registers its Layout Manager. - */ + // {@inheritDoc} + // Obtains the constraint layout holding the sensor measurement values. Initialises the Recycler + // View for holding WiFi data and registers its Layout Manager. @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); sensorMeasurementList = (ConstraintLayout) getView().findViewById(R.id.sensorMeasurementList); wifiListView = (RecyclerView) getView().findViewById(R.id.wifiList); wifiListView.setLayoutManager(new LinearLayoutManager(getActivity())); + bleListView = (RecyclerView) getView().findViewById(R.id.bleList); + bleListView.setLayoutManager(new LinearLayoutManager(getActivity())); } - /** - * Runnable task containing functionality to update the UI with the relevant sensor data. - * Must be run on the UI thread via a Handler. Obtains movement sensor values and the current - * WiFi networks from the {@link SensorFusion} instance and updates the UI with the new data - * and the string wrappers provided. - * - * @see SensorFusion class handling all sensors and data processing. - * @see Wifi class holding network data. - */ + // Runnable task containing functionality to update the UI with the relevant sensor data. + // Must be run on the UI thread via a Handler. Obtains movement sensor values and the current + // WiFi networks from the {@link SensorFusion} instance and updates the UI with the new data + // and the string wrappers provided. +// Related: SensorFusion class handling all sensors and data processing. +// Related: Wifi class holding network data. private final Runnable refreshTableTask = new Runnable() { @Override public void run() { // Get all the values from SensorFusion Map sensorValueMap = sensorFusion.getSensorValueMap(); - // Loop through UI elements and update the values - for(SensorTypes st : SensorTypes.values()) { - CardView cardView = (CardView) sensorMeasurementList.getChildAt(st.ordinal()); - ConstraintLayout currentRow = (ConstraintLayout) cardView.getChildAt(0); - float[] values = sensorValueMap.get(st); - for (int i = 0; i < values.length; i++) { - String valueString; - // Set string wrapper based on data type. - if(values.length == 1) { - valueString = getString(R.string.level, String.format("%.2f", values[0])); - } - else if(values.length == 2){ - if(st == SensorTypes.GNSSLATLONG) - valueString = getString(gnssPrefaces[i], String.format("%.2f", values[i])); - else - valueString = getString(prefaces[i], String.format("%.2f", values[i])); - } - else{ - valueString = getString(prefaces[i], String.format("%.2f", values[i])); - } - ((TextView) currentRow.getChildAt(i + 1)).setText(valueString); - } + + for (Map.Entry entry : sensorRowIds.entrySet()) { + updateSensorRow(entry.getKey(), entry.getValue(), sensorValueMap.get(entry.getKey())); } - // Get all WiFi values - convert to list of strings + + // Update WiFi list List wifiObjects = sensorFusion.getWifiList(); - // If there are WiFi networks visible, update the recycler view with the data. - if(wifiObjects != null) { - wifiListView.setAdapter(new WifiListAdapter(getActivity(), wifiObjects)); + if (wifiObjects != null && !wifiObjects.isEmpty()) { + android.util.Log.d("WiFiDebug", "Detected networks: " + wifiObjects.size()); + if (getContext() != null) { + wifiListView.setAdapter(new WifiListAdapter(getContext(), wifiObjects)); + } + } else { + android.util.Log.d("WiFiDebug", "No WiFi networks detected"); + // Create placeholder item for empty state + if (getContext() != null) { + List placeholderList = new ArrayList<>(); + Wifi placeholder = new Wifi(); + placeholder.setBssid(0); + placeholder.setSsid("Scanning..."); + placeholder.setLevel(-100); + placeholderList.add(placeholder); + wifiListView.setAdapter(new WifiListAdapter(getContext(), placeholderList)); + } } + + // Update BLE list + List bleDevices = sensorFusion.getBleList(); + if (bleDevices != null && !bleDevices.isEmpty()) { + android.util.Log.d("BLEDebug", "Detected BLE devices: " + bleDevices.size()); + if (getContext() != null) { + bleListView.setAdapter(new BleListAdapter(getContext(), bleDevices)); + } + } else { + android.util.Log.d("BLEDebug", "No BLE devices detected"); + // Create placeholder item for empty state + if (getContext() != null) { + List placeholderList = new ArrayList<>(); + BleDevice placeholder = new BleDevice("00:00:00:00:00:00", "Scanning...", -100); + placeholderList.add(placeholder); + bleListView.setAdapter(new BleListAdapter(getContext(), placeholderList)); + } + } + // Restart the data updater task in REFRESH_TIME milliseconds. refreshDataHandler.postDelayed(refreshTableTask, REFRESH_TIME); } }; -} \ No newline at end of file + + private void initialiseSensorRowIds() { + sensorRowIds.put(SensorTypes.ACCELEROMETER, R.id.accelerometerView); + sensorRowIds.put(SensorTypes.GRAVITY, R.id.gravityView); + sensorRowIds.put(SensorTypes.MAGNETICFIELD, R.id.magneticFieldView); + sensorRowIds.put(SensorTypes.GYRO, R.id.gyroscopeView); + sensorRowIds.put(SensorTypes.LIGHT, R.id.lightSensorView); + sensorRowIds.put(SensorTypes.PRESSURE, R.id.pressureSensorView); + sensorRowIds.put(SensorTypes.PROXIMITY, R.id.proximityView); + sensorRowIds.put(SensorTypes.GNSSLATLONG, R.id.gnssView); + sensorRowIds.put(SensorTypes.PDR, R.id.pdrView); + } + + private void updateSensorRow(SensorTypes sensorType, int cardId, float[] values) { + if (sensorMeasurementList == null || values == null) { + return; + } + + View view = sensorMeasurementList.findViewById(cardId); + if (!(view instanceof CardView)) { + return; + } + + CardView cardView = (CardView) view; + if (cardView.getChildCount() == 0 || !(cardView.getChildAt(0) instanceof ConstraintLayout)) { + return; + } + + ConstraintLayout currentRow = (ConstraintLayout) cardView.getChildAt(0); + for (int i = 0; i < values.length; i++) { + if (i + 1 >= currentRow.getChildCount()) { + break; + } + + String valueString; + if (values.length == 1) { + valueString = getString(R.string.level, String.format("%.2f", values[0])); + } else if (sensorType == SensorTypes.GNSSLATLONG) { + valueString = getString(gnssPrefaces[i], String.format("%.2f", values[i])); + } else { + valueString = getString(prefaces[i], String.format("%.2f", values[i])); + } + ((TextView) currentRow.getChildAt(i + 1)).setText(valueString); + } + } +} + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/RecordingFragment.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/RecordingFragment.java index 6362a971..8cf420cc 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/RecordingFragment.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/RecordingFragment.java @@ -1,298 +1,528 @@ package com.openpositioning.PositionMe.presentation.fragment; -import android.app.AlertDialog; -import android.content.Context; -import android.content.SharedPreferences; -import android.graphics.Color; import android.os.Bundle; -import android.os.CountDownTimer; import android.os.Handler; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - -import android.view.animation.AlphaAnimation; -import android.view.animation.Animation; -import android.view.animation.LinearInterpolator; import android.widget.Button; -import android.widget.ImageView; -import android.widget.ProgressBar; import android.widget.TextView; -import com.google.android.material.button.MaterialButton; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.biometric.BiometricManager; +import androidx.biometric.BiometricPrompt; +import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; -import androidx.preference.PreferenceManager; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.material.textfield.TextInputEditText; import com.openpositioning.PositionMe.R; import com.openpositioning.PositionMe.presentation.activity.RecordingActivity; +import com.openpositioning.PositionMe.sensors.Observer; import com.openpositioning.PositionMe.sensors.SensorFusion; import com.openpositioning.PositionMe.sensors.SensorTypes; -import com.openpositioning.PositionMe.utils.UtilFunctions; -import com.google.android.gms.maps.model.LatLng; +import com.openpositioning.PositionMe.utils.BuildingPolygon; +import com.openpositioning.PositionMe.utils.GeometryUtils; +import java.util.Locale; +import java.util.Map; -/** - * Fragment responsible for managing the recording process of trajectory data. - *

- * The RecordingFragment serves as the interface for users to initiate, monitor, and - * complete trajectory recording. It integrates sensor fusion data to track user movement - * and updates a map view in real time. Additionally, it provides UI controls to cancel, - * stop, and monitor recording progress. - *

- * Features: - * - Starts and stops trajectory recording. - * - Displays real-time sensor data such as elevation and distance traveled. - * - Provides UI controls to cancel or complete recording. - * - Uses {@link TrajectoryMapFragment} to visualize recorded paths. - * - Manages GNSS tracking and error display. - * - * @see TrajectoryMapFragment The map fragment displaying the recorded trajectory. - * @see RecordingActivity The activity managing the recording workflow. - * @see SensorFusion Handles sensor data collection. - * @see SensorTypes Enumeration of available sensor types. - * - * @author Shu Gu - */ - -public class RecordingFragment extends Fragment { - - // UI elements - private MaterialButton completeButton, cancelButton; - private ImageView recIcon; - private ProgressBar timeRemaining; - private TextView elevation, distanceTravelled, gnssError; - - // App settings - private SharedPreferences settings; - - // Sensor & data logic - private SensorFusion sensorFusion; - private Handler refreshDataHandler; - private CountDownTimer autoStop; - // Distance tracking - private float distance = 0f; - private float previousPosX = 0f; - private float previousPosY = 0f; +public class RecordingFragment extends Fragment implements Observer, IndoorMapFragment.VenueSelectionCallback { + + + private static final int AXIS_MODE = 1; + + + private static final float DISTANCE_MULTIPLIER = 1.0f; + + private static final double ROTATION_FINE_TUNE = 0.0; + + + + private Button startStopButton, markerButton; + private TextInputEditText trajectoryIdInput; + private TextView statusTextView; - // References to the child map fragment private TrajectoryMapFragment trajectoryMapFragment; + private SensorFusion sensorFusion; + private boolean isRecording = false; - private final Runnable refreshDataTask = new Runnable() { - @Override - public void run() { - updateUIandPosition(); - // Loop again - refreshDataHandler.postDelayed(refreshDataTask, 200); - } - }; + private BiometricPrompt biometricPrompt; + private BiometricPrompt.PromptInfo biometricPromptInfo; + private boolean biometricVerified = false; - public RecordingFragment() { - // Required empty public constructor - } + private Handler uiHandler = new Handler(); + private Runnable updateMapTask; + private boolean uiUpdatesRunning = false; - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - this.sensorFusion = SensorFusion.getInstance(); - Context context = requireActivity(); - this.settings = PreferenceManager.getDefaultSharedPreferences(context); - this.refreshDataHandler = new Handler(); - } + private String selectedBuildingId = null; + private String selectedVenueName = null; + + // PDR Variables + private LatLng pdrOrigin = null; + private float[] pdrStartOffset = null; + private LatLng currentPdrLocation = null; + private LatLng currentDisplayLocation = null; + private LatLng lastAbsoluteDistanceSample = null; + private static final double EARTH_RADIUS = 6378137.0; + private double totalDistanceMeters = 0.0; + private double absoluteDistanceMeters = 0.0; - @Nullable @Override - public View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - // Inflate only the "recording" UI parts (no map) + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_recording, container, false); } @Override - public void onViewCreated(@NonNull View view, - @Nullable Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - // Child Fragment: the container in fragment_recording.xml - // where TrajectoryMapFragment is placed - trajectoryMapFragment = (TrajectoryMapFragment) - getChildFragmentManager().findFragmentById(R.id.trajectoryMapFragmentContainer); - - // If not present, create it - if (trajectoryMapFragment == null) { - trajectoryMapFragment = new TrajectoryMapFragment(); - getChildFragmentManager() - .beginTransaction() - .replace(R.id.trajectoryMapFragmentContainer, trajectoryMapFragment) - .commit(); - } + sensorFusion = SensorFusion.getInstance(); - // Initialize UI references - elevation = view.findViewById(R.id.currentElevation); - distanceTravelled = view.findViewById(R.id.currentDistanceTraveled); - gnssError = view.findViewById(R.id.gnssError); - - completeButton = view.findViewById(R.id.stopButton); - cancelButton = view.findViewById(R.id.cancelButton); - recIcon = view.findViewById(R.id.redDot); - timeRemaining = view.findViewById(R.id.timeRemainingBar); - - // Hide or initialize default values - gnssError.setVisibility(View.GONE); - elevation.setText(getString(R.string.elevation, "0")); - distanceTravelled.setText(getString(R.string.meter, "0")); - - // Buttons - completeButton.setOnClickListener(v -> { - // Stop recording & go to correction - if (autoStop != null) autoStop.cancel(); - sensorFusion.stopRecording(); - // Show Correction screen - ((RecordingActivity) requireActivity()).showCorrectionScreen(); + trajectoryMapFragment = new TrajectoryMapFragment(); + trajectoryMapFragment.setOnVenueSelectedListener((buildingId, venueName) -> { + selectedBuildingId = buildingId; + selectedVenueName = venueName; + if (isRecording && statusTextView != null) { + updateRecordingStatus(); + } }); + getChildFragmentManager().beginTransaction() + .replace(R.id.mapFragmentContainer, trajectoryMapFragment) + .commit(); + + startStopButton = view.findViewById(R.id.bButton); + markerButton = view.findViewById(R.id.markerButton); + trajectoryIdInput = view.findViewById(R.id.trajectoryIdInput); + statusTextView = view.findViewById(R.id.statusText); + + + float[] startCoords = sensorFusion.getGNSSLatitude(true); + if (startCoords[0] != 0) { + new Handler().postDelayed(() -> { + if (trajectoryMapFragment != null && trajectoryMapFragment.isAdded()) { + trajectoryMapFragment.setInitialCameraPosition(new LatLng(startCoords[0], startCoords[1])); + } + }, 600); + } - - // Cancel button with confirmation dialog - cancelButton.setOnClickListener(v -> { - AlertDialog dialog = new AlertDialog.Builder(requireActivity()) - .setTitle("Confirm Cancel") - .setMessage("Are you sure you want to cancel the recording? Your progress will be lost permanently!") - .setNegativeButton("Yes", (dialogInterface, which) -> { - // User confirmed cancellation - sensorFusion.stopRecording(); - if (autoStop != null) autoStop.cancel(); - requireActivity().onBackPressed(); - }) - .setPositiveButton("No", (dialogInterface, which) -> { - // User cancelled the dialog. Do nothing. - dialogInterface.dismiss(); - }) - .create(); // Create the dialog but do not show it yet - - // Show the dialog and change the button color - dialog.setOnShowListener(dialogInterface -> { - Button negativeButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE); - negativeButton.setTextColor(Color.RED); // Set "Yes" button color to red - }); - - dialog.show(); // Finally, show the dialog + startStopButton.setOnClickListener(v -> { + if (!isRecording) { + if (biometricVerified) { + startRecording(); + } else { + authenticateAndStart(); + } + } else { + stopRecording(); + } }); - // The blinking effect for recIcon - blinkingRecordingIcon(); - - // Start the timed or indefinite UI refresh - if (this.settings.getBoolean("split_trajectory", false)) { - // A maximum recording time is set - long limit = this.settings.getInt("split_duration", 30) * 60000L; - timeRemaining.setMax((int) (limit / 1000)); - timeRemaining.setProgress(0); - timeRemaining.setScaleY(3f); - - autoStop = new CountDownTimer(limit, 1000) { - @Override - public void onTick(long millisUntilFinished) { - timeRemaining.incrementProgressBy(1); - updateUIandPosition(); + initBiometric(); + + markerButton.setOnClickListener(v -> { + if (isRecording) { + LatLng markerLocation = currentDisplayLocation != null ? currentDisplayLocation : currentPdrLocation; + if (markerLocation != null) { + sensorFusion.addMarkerAt(markerLocation, resolveDisplayAltitudeMeters()); + if (trajectoryMapFragment != null) { + trajectoryMapFragment.addMarkerToMap(markerLocation); + } + Toast.makeText(getContext(), "Marker Added", Toast.LENGTH_SHORT).show(); } + } + }); - @Override - public void onFinish() { - sensorFusion.stopRecording(); - ((RecordingActivity) requireActivity()).showCorrectionScreen(); - } - }.start(); + startUiUpdates(); + } + + private void initBiometric() { + BiometricManager biometricManager = BiometricManager.from(requireContext()); + if (biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) { + biometricPrompt = new BiometricPrompt(this, ContextCompat.getMainExecutor(requireContext()), + new BiometricPrompt.AuthenticationCallback() { + @Override + public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { + super.onAuthenticationError(errorCode, errString); + Log.w("RecordingFragment", "Biometric error: " + errString); + Toast.makeText(getContext(), "Fingerprint auth failed: " + errString, Toast.LENGTH_SHORT).show(); + } + + @Override + public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { + super.onAuthenticationSucceeded(result); + biometricVerified = true; + Toast.makeText(getContext(), "Fingerprint authentication succeeded", Toast.LENGTH_SHORT).show(); + startRecording(); + } + + @Override + public void onAuthenticationFailed() { + super.onAuthenticationFailed(); + Log.d("RecordingFragment", "Biometric auth failed."); + } + }); + + biometricPromptInfo = new BiometricPrompt.PromptInfo.Builder() + .setTitle("PositionMe Secure Recording") + .setSubtitle("Use fingerprint to authorize trajectory recording") + .setNegativeButtonText("Cancel") + .setDeviceCredentialAllowed(false) + .build(); } else { - // No set time limit, just keep refreshing - refreshDataHandler.post(refreshDataTask); + biometricPrompt = null; } } - /** - * Update the UI with sensor data and pass map updates to TrajectoryMapFragment. - */ - private void updateUIandPosition() { - float[] pdrValues = sensorFusion.getSensorValueMap().get(SensorTypes.PDR); - if (pdrValues == null) return; - - // Distance - distance += Math.sqrt(Math.pow(pdrValues[0] - previousPosX, 2) - + Math.pow(pdrValues[1] - previousPosY, 2)); - distanceTravelled.setText(getString(R.string.meter, String.format("%.2f", distance))); - - // Elevation - float elevationVal = sensorFusion.getElevation(); - elevation.setText(getString(R.string.elevation, String.format("%.1f", elevationVal))); - - // Current location - // Convert PDR coordinates to actual LatLng if you have a known starting lat/lon - // Or simply pass relative data for the TrajectoryMapFragment to handle - // For example: - float[] latLngArray = sensorFusion.getGNSSLatitude(true); - if (latLngArray != null) { - LatLng oldLocation = trajectoryMapFragment.getCurrentLocation(); // or store locally - LatLng newLocation = UtilFunctions.calculateNewPos( - oldLocation == null ? new LatLng(latLngArray[0], latLngArray[1]) : oldLocation, - new float[]{ pdrValues[0] - previousPosX, pdrValues[1] - previousPosY } - ); - - // Pass the location + orientation to the map - if (trajectoryMapFragment != null) { - trajectoryMapFragment.updateUserLocation(newLocation, - (float) Math.toDegrees(sensorFusion.passOrientation())); - } + private void authenticateAndStart() { + if (biometricPrompt != null && biometricPromptInfo != null) { + biometricPrompt.authenticate(biometricPromptInfo); + } else { + // Biometric unavailable, fallback with passwordless confirmation + biometricVerified = true; + startRecording(); } + } - // GNSS logic if you want to show GNSS error, etc. - float[] gnss = sensorFusion.getSensorValueMap().get(SensorTypes.GNSSLATLONG); - if (gnss != null && trajectoryMapFragment != null) { - // If user toggles showing GNSS in the map, call e.g. - if (trajectoryMapFragment.isGnssEnabled()) { - LatLng gnssLocation = new LatLng(gnss[0], gnss[1]); - LatLng currentLoc = trajectoryMapFragment.getCurrentLocation(); - if (currentLoc != null) { - double errorDist = UtilFunctions.distanceBetweenPoints(currentLoc, gnssLocation); - gnssError.setVisibility(View.VISIBLE); - gnssError.setText(String.format(getString(R.string.gnss_error) + "%.2fm", errorDist)); + private void startRecording() { + String id = ""; + if (trajectoryIdInput.getText() != null) { + id = trajectoryIdInput.getText().toString().trim(); + } + if (id.isEmpty()) { + id = "Traj_" + System.currentTimeMillis(); + } + + Log.e("RecordingFragment", "Starting recording with dynamic origin policy: " + id); + pdrOrigin = resolveRecordingOrigin(); + if (pdrOrigin != null) { + sensorFusion.setStartGNSSLatitude(new float[]{(float) pdrOrigin.latitude, (float) pdrOrigin.longitude}); + } + + sensorFusion.startRecording(id); + + if (selectedVenueName != null) sensorFusion.setVenueName(selectedVenueName); + if (selectedBuildingId != null) sensorFusion.setBuildingId(selectedBuildingId); + + isRecording = true; + totalDistanceMeters = 0.0; + absoluteDistanceMeters = 0.0; + + // Lock start point (Manual Origin) + if (pdrOrigin != null) { + currentPdrLocation = pdrOrigin; + currentDisplayLocation = pdrOrigin; + lastAbsoluteDistanceSample = pdrOrigin; + + // Record initial offset + Map sensorData = sensorFusion.getSensorValueMap(); + if (sensorData != null) { + float[] currentPDR = sensorData.get(SensorTypes.PDR); + if (currentPDR != null) { + pdrStartOffset = new float[]{currentPDR[0], currentPDR[1]}; + } else { + pdrStartOffset = new float[]{0f, 0f}; } - trajectoryMapFragment.updateGNSS(gnssLocation); - } else { - gnssError.setVisibility(View.GONE); - trajectoryMapFragment.clearGNSS(); } } - // Update previous - previousPosX = pdrValues[0]; - previousPosY = pdrValues[1]; + if (trajectoryMapFragment != null) { + trajectoryMapFragment.detectCurrentFloorOnce(); + } + + updateRecordingStatus(); + statusTextView.setBackgroundResource(R.drawable.status_recording); + + startStopButton.setText("Stop"); + startStopButton.setBackgroundColor(getResources().getColor(R.color.ios_red)); + trajectoryIdInput.setEnabled(false); + markerButton.setEnabled(true); + + startUiUpdates(); + } + + private void startUiUpdates() { + if (uiUpdatesRunning) { + return; + } + uiUpdatesRunning = true; + + updateMapTask = new Runnable() { + @Override + public void run() { + if (isAdded()) { + Map sensorData = sensorFusion.getSensorValueMap(); + + if (sensorData != null) { + float orientation = sensorFusion.passOrientation(); + + // Keep GNSS/WiFi visible in initial screen even before pressing Start. + float[] gnssPos = sensorData.get(SensorTypes.GNSSLATLONG); + if (gnssPos != null && gnssPos[0] != 0) { + if (trajectoryMapFragment != null) { + trajectoryMapFragment.updateGNSS(new LatLng(gnssPos[0], gnssPos[1])); + } + } + + float[] wifiPos = sensorData.get(SensorTypes.WIFI); + if (wifiPos != null && wifiPos[0] != 0 && wifiPos[1] != 0) { + if (trajectoryMapFragment != null) { + trajectoryMapFragment.updateWifi(new LatLng(wifiPos[0], wifiPos[1])); + } + } + + if (isRecording) { + float[] pdrMovement = sensorData.get(SensorTypes.PDR); + float[] fusedPos = sensorData.get(SensorTypes.FUSED); + + LatLng fusedLocation = null; + if (fusedPos != null && fusedPos[0] != 0 && fusedPos[1] != 0) { + fusedLocation = new LatLng(fusedPos[0], fusedPos[1]); + } + + // PDR Trajectory Logic + if (pdrOrigin != null && pdrMovement != null) { + + // A. Calculate raw relative displacement + float startX = (pdrStartOffset != null) ? pdrStartOffset[0] : 0; + float startY = (pdrStartOffset != null) ? pdrStartOffset[1] : 0; + float rawX = pdrMovement[0] - startX; + float rawY = pdrMovement[1] - startY; + + // B. [Key] Axis Mapping Correction + float mapX = rawX; + float mapY = rawY; + + switch (AXIS_MODE) { + case 1: // Standard + mapX = rawX; mapY = rawY; + break; + case 2: // Swap XY - Solves common "Sin/Cos swapped" issue + mapX = rawY; mapY = rawX; + break; + case 3: // Flip Y - Solves North/South inversion + mapX = rawX; mapY = -rawY; + break; + case 4: // Flip X - Solves East/West inversion + mapX = -rawX; mapY = rawY; + break; + } + + // C. Apply Distance Multiplier + mapX *= DISTANCE_MULTIPLIER; + mapY *= DISTANCE_MULTIPLIER; + + // D. Apply Fine Tune Rotation + double theta = Math.toRadians(ROTATION_FINE_TUNE); + double rotatedX = mapX * Math.cos(theta) - mapY * Math.sin(theta); + double rotatedY = mapX * Math.sin(theta) + mapY * Math.cos(theta); + + // E. Convert to LatLng and Update + LatLng newLocation = calculateLatLngFromMeters(pdrOrigin, (float)rotatedX, (float)rotatedY); + currentPdrLocation = newLocation; + totalDistanceMeters = Math.sqrt(rotatedX*rotatedX + rotatedY*rotatedY); + + if (trajectoryMapFragment != null) { + // Keep trajectory direction fully PDR-driven during recording. + LatLng primaryLocation = newLocation; + updateAbsoluteDistance(primaryLocation); + currentDisplayLocation = primaryLocation; + trajectoryMapFragment.updateUserLocation(primaryLocation, orientation); + } + + if (statusTextView != null && isAdded()) { + String venueSuffix = selectedVenueName != null ? "\n" + selectedVenueName : ""; + statusTextView.setText("Recording" + buildErrorStatusSuffix() + venueSuffix); + } + } else if (fusedLocation != null && trajectoryMapFragment != null) { + updateAbsoluteDistance(fusedLocation); + currentDisplayLocation = fusedLocation; + trajectoryMapFragment.updateUserLocation(fusedLocation, orientation); + if (statusTextView != null && isAdded()) { + String venueSuffix = selectedVenueName != null ? "\n" + selectedVenueName : ""; + statusTextView.setText("Recording\nFusion locked" + buildErrorStatusSuffix() + venueSuffix); + } + } + } + } + uiHandler.postDelayed(this, 100); + } + } + }; + uiHandler.post(updateMapTask); } - /** - * Start the blinking effect for the recording icon. - */ - private void blinkingRecordingIcon() { - Animation blinking = new AlphaAnimation(1, 0); - blinking.setDuration(800); - blinking.setInterpolator(new LinearInterpolator()); - blinking.setRepeatCount(Animation.INFINITE); - blinking.setRepeatMode(Animation.REVERSE); - recIcon.startAnimation(blinking); + private LatLng calculateLatLngFromMeters(LatLng origin, float xMeters, float yMeters) { + double lat = origin.latitude; + double dLat = (yMeters / EARTH_RADIUS) * (180 / Math.PI); + double cosLat = Math.cos(Math.toRadians(lat)); + if (Math.abs(cosLat) < 0.000001) cosLat = 0.000001; + double dLon = (xMeters / (EARTH_RADIUS * cosLat)) * (180 / Math.PI); + return new LatLng(lat + dLat, origin.longitude + dLon); } - @Override - public void onPause() { - super.onPause(); - refreshDataHandler.removeCallbacks(refreshDataTask); + private void stopRecording() { + isRecording = false; + sensorFusion.stopRecording(); + pdrOrigin = null; + pdrStartOffset = null; + currentPdrLocation = null; + currentDisplayLocation = null; + lastAbsoluteDistanceSample = null; + totalDistanceMeters = 0.0; + absoluteDistanceMeters = 0.0; + startStopButton.setText("Start"); + startStopButton.setBackgroundColor(getResources().getColor(R.color.ios_blue)); + markerButton.setEnabled(false); + trajectoryIdInput.setEnabled(true); + statusTextView.setText("Recording stopped"); + statusTextView.setBackgroundResource(R.drawable.status_background); + if (getActivity() instanceof RecordingActivity) { + ((RecordingActivity) getActivity()).showCorrectionScreen(); + } } @Override - public void onResume() { - super.onResume(); - if(!this.settings.getBoolean("split_trajectory", false)) { - refreshDataHandler.postDelayed(refreshDataTask, 500); + public void onDestroyView() { + super.onDestroyView(); + if (updateMapTask != null) { + uiHandler.removeCallbacks(updateMapTask); + } + uiUpdatesRunning = false; + } + + @Override public void update(Object[] data) { } + @Override public void onVenueSelected(String buildingId, String venueName) { + this.selectedBuildingId = buildingId; + this.selectedVenueName = venueName; + if (isRecording && statusTextView != null) { + updateRecordingStatus(); + } + } + + private LatLng resolveRecordingOrigin() { + LatLng wifiLocation = sensorFusion.getLatLngWifiPositioning(); + boolean indoorBySelection = selectedBuildingId != null && !selectedBuildingId.isEmpty(); + if (wifiLocation != null && (indoorBySelection || isInsideKnownIndoorBuildings(wifiLocation))) { + return wifiLocation; + } + + float[] gnssCoords = sensorFusion.getGNSSLatitude(false); + LatLng gnssLocation = null; + if (gnssCoords[0] != 0 || gnssCoords[1] != 0) { + gnssLocation = new LatLng(gnssCoords[0], gnssCoords[1]); + } + + if (gnssLocation != null && !indoorBySelection && !isInsideKnownIndoorBuildings(gnssLocation)) { + return gnssLocation; + } + + if (wifiLocation != null) { + return wifiLocation; + } + + if (gnssLocation != null) { + return gnssLocation; } + + float[] startCoords = sensorFusion.getGNSSLatitude(true); + if (startCoords[0] != 0 || startCoords[1] != 0) { + return new LatLng(startCoords[0], startCoords[1]); + } + + return null; + } + + private boolean isInsideKnownIndoorBuildings(@NonNull LatLng point) { + return BuildingPolygon.inAnyKnownBuilding(point); + } + + private LatLng blendLocations(LatLng primary, LatLng correction, double correctionWeight) { + if (primary == null) { + return correction; + } + if (correction == null) { + return primary; + } + double w = Math.max(0.0, Math.min(0.25, correctionWeight)); + double lat = primary.latitude + w * (correction.latitude - primary.latitude); + double lon = primary.longitude + w * (correction.longitude - primary.longitude); + return new LatLng(lat, lon); + } + + private float resolveDisplayAltitudeMeters() { + float estimatedAltitude = sensorFusion.getEstimatedAbsoluteAltitude(); + if (!Float.isNaN(estimatedAltitude)) { + return estimatedAltitude; + } + return Float.NaN; + } + + private String buildErrorStatusSuffix() { + return "\nErr PDR: " + formatLocationError(resolvePdrLocation(), currentDisplayLocation) + + " GNSS: " + formatLocationError(resolveGnssLocation(), currentDisplayLocation) + + " WiFi: " + formatLocationError(resolveWifiLocation(), currentDisplayLocation); + } + + private void updateAbsoluteDistance(LatLng currentLocation) { + if (currentLocation == null) { + return; + } + + if (lastAbsoluteDistanceSample != null) { + absoluteDistanceMeters += GeometryUtils.distanceBetween(lastAbsoluteDistanceSample, currentLocation); + } + lastAbsoluteDistanceSample = currentLocation; + } + + private String formatDistance(double distanceMeters) { + return distanceMeters < 1000 + ? String.format(Locale.US, "%.1f m", distanceMeters) + : String.format(Locale.US, "%.2f km", distanceMeters / 1000.0); + } + + private LatLng resolvePdrLocation() { + return currentPdrLocation; + } + + private LatLng resolveGnssLocation() { + float[] gnssCoords = sensorFusion.getGNSSLatitude(false); + if ((gnssCoords[0] == 0f && gnssCoords[1] == 0f)) { + return null; + } + return new LatLng(gnssCoords[0], gnssCoords[1]); + } + + private LatLng resolveWifiLocation() { + return sensorFusion.getLatLngWifiPositioning(); + } + + private String formatLocationError(LatLng sourceLocation, LatLng referenceLocation) { + if (sourceLocation == null || referenceLocation == null) { + return "--"; + } + + double distanceMeters = GeometryUtils.distanceBetween(sourceLocation, referenceLocation); + return distanceMeters < 1000 + ? String.format(Locale.US, "%.1f m", distanceMeters) + : String.format(Locale.US, "%.2f km", distanceMeters / 1000.0); + } + + private void updateRecordingStatus() { + if (statusTextView == null) { + return; + } + + StringBuilder status = new StringBuilder("Recording"); + status.append(buildErrorStatusSuffix()); + if (selectedVenueName != null && !selectedVenueName.isEmpty()) { + status.append("\n").append(selectedVenueName); + } + statusTextView.setText(status.toString()); } } + 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 d15a4a83..4262f70e 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 @@ -8,6 +8,7 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.SeekBar; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -18,47 +19,39 @@ import com.openpositioning.PositionMe.R; import com.openpositioning.PositionMe.presentation.activity.ReplayActivity; import com.openpositioning.PositionMe.data.local.TrajParser; +import com.openpositioning.PositionMe.utils.TrajectoryVerifier; import java.io.File; import java.util.ArrayList; import java.util.List; -/** - * Sub fragment of Replay Activity. Fragment that replays trajectory data on a map. - *

- * The ReplayFragment is responsible for visualizing and replaying trajectory data captured during - * previous recordings. It loads trajectory data from a JSON file, updates the map with user movement, - * and provides UI controls for playback, pause, and seek functionalities. - *

- * Features: - * - Loads trajectory data from a file and displays it on a map. - * - Provides playback controls including play, pause, restart, and go to end. - * - Updates the trajectory dynamically as playback progresses. - * - Allows users to manually seek through the recorded trajectory. - * - Integrates with {@link TrajectoryMapFragment} for map visualization. - * - * @see TrajectoryMapFragment The map fragment displaying the trajectory. - * @see ReplayActivity The activity managing the replay workflow. - * @see TrajParser Utility class for parsing trajectory data. - * - * @author Shu Gu - */ +// This fragment controls the full replay lifecycle for one trajectory file. +// It receives replay arguments, validates file integrity, allows start-point +// selection, and renders frames onto the map in playback order. +// +// Replay pipeline implemented in this class: +// Read replay arguments from ReplayActivity. +// Verify and parse replay data from TrajParser. +// Dispatch draw updates to TrajectoryMapFragment. +// Keep UI state, seek bar position, and playback index synchronized. + public class ReplayFragment extends Fragment { private static final String TAG = "ReplayFragment"; - // GPS start location (received from ReplayActivity) + // Manual start position received from ReplayActivity. private float initialLat = 0f; private float initialLon = 0f; + private LatLng recordedStartPoint = null; private String filePath = ""; private int lastIndex = -1; - // UI Controls + // UI controls used to drive replay interaction. private TrajectoryMapFragment trajectoryMapFragment; private Button playPauseButton, restartButton, exitButton, goEndButton; private SeekBar playbackSeekBar; - // Playback-related + // Playback state and scheduling fields. private final Handler playbackHandler = new Handler(); private final long PLAYBACK_INTERVAL_MS = 500; // milliseconds private List replayData = new ArrayList<>(); @@ -69,20 +62,20 @@ public class ReplayFragment extends Fragment { public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Retrieve transferred data from ReplayActivity + // Read replay arguments passed by ReplayActivity. if (getArguments() != null) { filePath = getArguments().getString(ReplayActivity.EXTRA_TRAJECTORY_FILE_PATH, ""); initialLat = getArguments().getFloat(ReplayActivity.EXTRA_INITIAL_LAT, 0f); initialLon = getArguments().getFloat(ReplayActivity.EXTRA_INITIAL_LON, 0f); } - // Log the received data + // Log incoming replay context for debugging and support traces. Log.i(TAG, "ReplayFragment received data:"); Log.i(TAG, "Trajectory file path: " + filePath); Log.i(TAG, "Initial latitude: " + initialLat); Log.i(TAG, "Initial longitude: " + initialLon); - // Check if file exists before parsing + // Validate source file accessibility before parsing. File trajectoryFile = new File(filePath); if (!trajectoryFile.exists()) { Log.e(TAG, "ERROR: Trajectory file does NOT exist at: " + filePath); @@ -95,14 +88,22 @@ 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); + // Perform structural verification before building replay frames. + boolean isValid = TrajectoryVerifier.verifyTrajectoryFile(filePath); + if (!isValid) { + Log.e(TAG, "Trajectory file verification FAILED - file may be corrupt or empty"); + } - // Log the number of parsed points - if (replayData != null && !replayData.isEmpty()) { - Log.i(TAG, "Trajectory data loaded successfully. Total points: " + replayData.size()); + // Detect which start-point sources are available in this file. + boolean gnssExists = TrajParser.hasGnssData(filePath); + recordedStartPoint = TrajParser.getRecordedInitialPoint(filePath); + + if (gnssExists || recordedStartPoint != null) { + showStartChoiceDialog(gnssExists, recordedStartPoint != null); } else { - Log.e(TAG, "Failed to load trajectory data! replayData is empty or null."); + // Fall back to manual start when file-based sources are unavailable. + Log.i(TAG, "No GNSS/recorded start in file, using manual start location."); + loadTrajectory(initialLat, initialLon); } } @@ -120,7 +121,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - // Initialize map fragment + // Attach or create the child map fragment used for rendering frames. trajectoryMapFragment = (TrajectoryMapFragment) getChildFragmentManager().findFragmentById(R.id.replayMapFragmentContainer); if (trajectoryMapFragment == null) { @@ -130,39 +131,27 @@ public void onViewCreated(@NonNull View view, .replace(R.id.replayMapFragmentContainer, trajectoryMapFragment) .commit(); } + + // Replay data can be loaded before map readiness. Rendering methods are + // written to tolerate this ordering and apply state when the map is ready. - - - // 1) Check if the file contains any GNSS data - boolean gnssExists = hasAnyGnssData(replayData); - - if (gnssExists) { - showGnssChoiceDialog(); - } else { - // No GNSS data -> automatically use param lat/lon - if (initialLat != 0f || initialLon != 0f) { - LatLng startPoint = new LatLng(initialLat, initialLon); - Log.i(TAG, "Setting initial map position: " + startPoint.toString()); - trajectoryMapFragment.setInitialCameraPosition(startPoint); - } - } - - // Initialize UI controls + // Bind playback controls. playPauseButton = view.findViewById(R.id.playPauseButton); restartButton = view.findViewById(R.id.restartButton); exitButton = view.findViewById(R.id.exitButton); goEndButton = view.findViewById(R.id.goEndButton); playbackSeekBar = view.findViewById(R.id.playbackSeekBar); - // Set SeekBar max value based on replay data + // Initialize seek range when parsed data is already available. if (!replayData.isEmpty()) { playbackSeekBar.setMax(replayData.size() - 1); } - // Button Listeners + // Playback toggle. playPauseButton.setOnClickListener(v -> { if (replayData.isEmpty()) { Log.w(TAG, "Play/Pause button pressed but replayData is empty."); + Toast.makeText(requireContext(), "This trajectory has no replayable points.", Toast.LENGTH_SHORT).show(); return; } if (isPlaying) { @@ -180,7 +169,7 @@ public void onViewCreated(@NonNull View view, } }); - // Restart button listener + // Restart from first frame. restartButton.setOnClickListener(v -> { if (replayData.isEmpty()) return; currentIndex = 0; @@ -189,7 +178,7 @@ public void onViewCreated(@NonNull View view, updateMapForIndex(0); }); - // Go to End button listener + // Jump to final frame and pause playback. goEndButton.setOnClickListener(v -> { if (replayData.isEmpty()) return; currentIndex = replayData.size() - 1; @@ -200,7 +189,7 @@ public void onViewCreated(@NonNull View view, playPauseButton.setText("Play"); }); - // Exit button listener + // Exit replay flow and return to the previous screen. exitButton.setOnClickListener(v -> { Log.i(TAG, "Exit button pressed. Exiting replay."); if (getActivity() instanceof ReplayActivity) { @@ -210,7 +199,7 @@ public void onViewCreated(@NonNull View view, } }); - // SeekBar listener + // Scrub to a user-selected frame. playbackSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { @@ -231,69 +220,78 @@ 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) { - if (point.gnssLocation != null) { - return true; - } - } - return false; - } - - /** - * Show a simple dialog asking user to pick: - * 1) GNSS from file - * 2) Lat/Lon from ReplayActivity arguments - */ - private void showGnssChoiceDialog() { - new AlertDialog.Builder(requireContext()) + // Show start-source selection based on available trajectory metadata. + private void showStartChoiceDialog(boolean hasGnss, boolean hasRecordedStart) { + AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()) .setTitle("Choose Starting Location") - .setMessage("GNSS data is found in the file. Would you like to use the file's GNSS as the start, or the one you manually picked?") - .setPositiveButton("Use File's GNSS", (dialog, which) -> { - LatLng firstGnss = getFirstGnssLocation(replayData); - if (firstGnss != null) { - setupInitialMapPosition((float) firstGnss.latitude, (float) firstGnss.longitude); - } else { - // Fallback if no valid GNSS found - setupInitialMapPosition(initialLat, initialLon); - } - dialog.dismiss(); - }) + .setMessage("Select which source should be used as replay trajectory start.") .setNegativeButton("Use Manual Set", (dialog, which) -> { - setupInitialMapPosition(initialLat, initialLon); + loadTrajectory(initialLat, initialLon); dialog.dismiss(); }) - .setCancelable(false) - .show(); + .setCancelable(false); + + if (hasGnss) { + builder.setPositiveButton("Use File's GNSS", (dialog, which) -> { + LatLng firstGnss = TrajParser.getFirstGnssPoint(filePath); + if (firstGnss != null) { + loadTrajectory((float) firstGnss.latitude, (float) firstGnss.longitude); + } else { + loadTrajectory(initialLat, initialLon); + } + dialog.dismiss(); + }); + } + + if (hasRecordedStart && recordedStartPoint != null) { + builder.setNeutralButton("Use Recorded Start", (dialog, which) -> { + loadTrajectory((float) recordedStartPoint.latitude, (float) recordedStartPoint.longitude); + dialog.dismiss(); + }); + } + + builder.show(); } - private void setupInitialMapPosition(float latitude, float longitude) { - LatLng startPoint = new LatLng(initialLat, initialLon); + private void loadTrajectory(float latitude, float longitude) { + lastIndex = -1; + + // Configure camera anchor and replay start marker before parsing. + LatLng startPoint = new LatLng(latitude, longitude); + LatLng recordedStartPoint = TrajParser.getRecordedInitialPoint(filePath); Log.i(TAG, "Setting initial map position: " + startPoint.toString()); - trajectoryMapFragment.setInitialCameraPosition(startPoint); - } + if (trajectoryMapFragment != null) { + trajectoryMapFragment.setInitialCameraPosition(startPoint); + trajectoryMapFragment.setReplayStartMarker(recordedStartPoint != null ? recordedStartPoint : startPoint); + } - /** - * Retrieve the first available GNSS location from the replay data. - */ - private LatLng getFirstGnssLocation(List data) { - for (TrajParser.ReplayPoint point : data) { - if (point.gnssLocation != null) { - return new LatLng(replayData.get(0).gnssLocation.latitude, replayData.get(0).gnssLocation.longitude); + // Parse replay points using the selected start origin. + replayData = TrajParser.parseTrajectoryData(filePath, requireContext(), latitude, longitude); + + // Apply parsed result to UI. + if (replayData != null && !replayData.isEmpty()) { + Log.i(TAG, "Trajectory data loaded successfully. Total points: " + replayData.size()); + // Update seek bounds to match available frames. + if (playbackSeekBar != null) { + playbackSeekBar.setMax(replayData.size() - 1); + playbackSeekBar.setProgress(0); } + // Draw initial frame immediately. + updateMapForIndex(0); + } else { + Log.e(TAG, "Failed to load trajectory data!"); + if (playPauseButton != null) { + playPauseButton.setEnabled(false); + } + Toast.makeText(requireContext(), "Replay data is empty for this file.", Toast.LENGTH_LONG).show(); } - return null; // None found } - /** - * Runnable for playback of trajectory data. - * This runnable is called repeatedly to update the map with the next point in the replayData list. - */ + + + // Periodic task that advances playback by one frame on each tick. private final Runnable playbackRunnable = new Runnable() { @Override public void run() { @@ -315,41 +313,42 @@ public void run() { }; - /** - * Update the map with the user location and GNSS location (if available) for the given index. - * Clears the map and redraws up to the given index. - * - * @param newIndex - */ + // Render replay state at target frame index. + // Forward playback uses incremental drawing. Backward jumps trigger full redraw. private void updateMapForIndex(int newIndex) { if (newIndex < 0 || newIndex >= replayData.size()) return; + if (trajectoryMapFragment == null) return; - // Detect if user is playing sequentially (lastIndex + 1) - // or is skipping around (backwards, or jump forward) - boolean isSequentialForward = (newIndex == lastIndex + 1); - - if (!isSequentialForward) { - // Clear everything and redraw up to newIndex + if (lastIndex == -1 || newIndex < lastIndex) { trajectoryMapFragment.clearMapAndReset(); - for (int i = 0; i <= newIndex; i++) { - TrajParser.ReplayPoint p = replayData.get(i); - trajectoryMapFragment.updateUserLocation(p.pdrLocation, p.orientation); - if (p.gnssLocation != null) { - trajectoryMapFragment.updateGNSS(p.gnssLocation); - } - } - } else { - // Normal sequential forward step: add just the new point - TrajParser.ReplayPoint p = replayData.get(newIndex); - trajectoryMapFragment.updateUserLocation(p.pdrLocation, p.orientation); - if (p.gnssLocation != null) { - trajectoryMapFragment.updateGNSS(p.gnssLocation); - } + renderRange(0, newIndex); + lastIndex = newIndex; + return; } + if (newIndex == lastIndex) { + return; + } + + renderRange(lastIndex + 1, newIndex); + lastIndex = newIndex; } + private void renderRange(int startIndex, int endIndex) { + for (int i = startIndex; i <= endIndex; i++) { + TrajParser.ReplayPoint point = replayData.get(i); + + trajectoryMapFragment.updateUserLocation(point.pdrLocation, point.orientation); + if (point.gnssLocation != null) { + trajectoryMapFragment.updateGNSS(point.gnssLocation); + } + if (point.wifiLocation != null) { + trajectoryMapFragment.updateWifi(point.wifiLocation); + } + } + } + @Override public void onPause() { super.onPause(); @@ -363,3 +362,6 @@ public void onDestroyView() { playbackHandler.removeCallbacks(playbackRunnable); } } + + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/SettingsFragment.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/SettingsFragment.java index c1f6501c..6cbcf202 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/SettingsFragment.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/SettingsFragment.java @@ -8,14 +8,10 @@ import com.openpositioning.PositionMe.R; -/** - * SettingsFragment that inflates and displays the preferences (settings). - * Sets type for numeric only fields. - * - * @see HomeFragment the return fragment when leaving the settings. - * - * @author Mate Stodulka - */ +// SettingsFragment that inflates and displays the preferences (settings). +// Sets type for numeric only fields. +// Related: HomeFragment the return fragment when leaving the settings. +// @author Mate Stodulka public class SettingsFragment extends PreferenceFragmentCompat { // EditTextPreference fields with numeric only inputs accepted. @@ -26,10 +22,8 @@ public class SettingsFragment extends PreferenceFragmentCompat { private EditTextPreference accelFilter; private EditTextPreference wifiInterval; - /** - * {@inheritDoc} - * Sets the relevant numeric type for the preferences that should not take string values. - */ + // {@inheritDoc} + // Sets the relevant numeric type for the preferences that should not take string values. @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.root_preferences, rootKey); @@ -54,4 +48,5 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { InputType.TYPE_CLASS_NUMBER)); } -} \ No newline at end of file +} + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/StartLocationFragment.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/StartLocationFragment.java index ee14f69f..b200a61f 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/StartLocationFragment.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/StartLocationFragment.java @@ -16,24 +16,22 @@ 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.presentation.activity.RecordingActivity; import com.openpositioning.PositionMe.presentation.activity.ReplayActivity; import com.openpositioning.PositionMe.sensors.SensorFusion; -import com.openpositioning.PositionMe.utils.NucleusBuildingManager; - -/** - * A simple {@link Fragment} subclass. The startLocation fragment is displayed before the trajectory - * recording starts. This fragment displays a map in which the user can adjust their location to - * correct the PDR when it is complete - * - * @author Virginia Cangelosi - * @see HomeFragment the previous fragment in the nav graph. - * @see RecordingFragment the next fragment in the nav graph. - * @see SensorFusion the class containing sensors and recording. - */ +import com.openpositioning.PositionMe.utils.IndoorMapManager; +import com.google.android.gms.maps.model.Polygon; +import android.widget.Toast; + +// A simple {@link Fragment} subclass. The startLocation fragment is displayed before the trajectory +// recording starts. This fragment displays a map in which the user can adjust their location to +// correct the PDR when it is complete. +// @author Virginia Cangelosi +// Related: HomeFragment the previous fragment in the nav graph. +// Related: RecordingFragment the next fragment in the nav graph. +// Related: SensorFusion the class containing sensors and recording. public class StartLocationFragment extends Fragment { // Button to go to next fragment and save the location @@ -46,23 +44,19 @@ public class StartLocationFragment extends Fragment { private float[] startPosition = new float[2]; // Zoom level for the Google map private float zoom = 19f; - // Instance for managing indoor building overlays (if any) - private NucleusBuildingManager nucleusBuildingManager; - // Dummy variable for floor index - private int FloorNK; - - /** - * Public Constructor for the class. - * Left empty as not required - */ + // Instance for managing indoor building overlays using new API-based system + private IndoorMapManager indoorMapManager; + // Google map instance + private GoogleMap googleMap; + + // 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 - */ + // {@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) { @@ -85,83 +79,73 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, SupportMapFragment supportMapFragment = (SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.startMap); - 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) { - // Set map type and UI settings - mMap.setMapType(GoogleMap.MAP_TYPE_HYBRID); - mMap.getUiSettings().setCompassEnabled(true); - mMap.getUiSettings().setTiltGesturesEnabled(true); - mMap.getUiSettings().setRotateGesturesEnabled(true); - mMap.getUiSettings().setScrollGesturesEnabled(true); - - // *** FIX: Clear any existing markers so the start marker isn’t duplicated *** - mMap.clear(); - - // Create NucleusBuildingManager instance (if needed) - nucleusBuildingManager = new NucleusBuildingManager(mMap); - nucleusBuildingManager.getIndoorMapManager().hideMap(); - - // Add a marker at the current GPS location and move the camera - position = new LatLng(startPosition[0], startPosition[1]); - Marker startMarker = mMap.addMarker(new MarkerOptions() - .position(position) - .title("Start Position") - .draggable(true)); - mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(position, zoom)); - - // Drag listener for the marker to update the start position when dragged - mMap.setOnMarkerDragListener(new GoogleMap.OnMarkerDragListener() { - /** - * {@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) {} - }); - } - }); + if (supportMapFragment != null) { + 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) { + googleMap = mMap; + + // Set map type and UI settings + mMap.setMapType(GoogleMap.MAP_TYPE_HYBRID); + mMap.getUiSettings().setCompassEnabled(true); + mMap.getUiSettings().setTiltGesturesEnabled(true); + mMap.getUiSettings().setRotateGesturesEnabled(true); + mMap.getUiSettings().setScrollGesturesEnabled(true); + + // Clear any existing markers + mMap.clear(); + + // Initialize new IndoorMapManager (supports all buildings via API) + indoorMapManager = new IndoorMapManager(mMap, requireContext()); + + // Load all building outlines (Nucleus, Library, FJB, Murchison) + indoorMapManager.addFallbackBuildings(); + + // Try to fetch building data from API + LatLng kbCampus = new LatLng(55.9230, -3.1750); + indoorMapManager.fetchBuildingsFromApi(kbCampus); + + // Set up building polygon click listener + mMap.setOnPolygonClickListener(new GoogleMap.OnPolygonClickListener() { + @Override + public void onPolygonClick(@NonNull Polygon polygon) { + if (indoorMapManager != null) { + boolean handled = indoorMapManager.onPolygonClick(polygon); + if (handled) { + String buildingName = indoorMapManager.getSelectedBuildingName(); + Toast.makeText(getContext(), "Selected: " + buildingName, Toast.LENGTH_SHORT).show(); + } + } + } + }); + + // Add a marker at the current GPS location and move the camera + position = new LatLng(startPosition[0], startPosition[1]); + mMap.addMarker(new MarkerOptions() + .position(position) + .title("Start Position")); + mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(position, zoom)); + } + }); + } return rootView; } - /** - * {@inheritDoc} - * Button onClick listener enabled to detect when to go to next fragment and start PDR recording. - */ + // {@inheritDoc} + // Button onClick listener enabled to detect when to go to next fragment. + // NOTE: Actual recording start is now deferred to RecordingFragment. @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); this.button = view.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) { float chosenLat = startPosition[0]; @@ -169,38 +153,19 @@ public void onClick(View view) { // If the Activity is RecordingActivity if (requireActivity() instanceof RecordingActivity) { - // Start sensor recording + set the start location - sensorFusion.startRecording(); - sensorFusion.setStartGNSSLatitude(startPosition); + // Clear manual start so recording can choose WiFi/GNSS dynamically. + sensorFusion.setStartGNSSLatitude(new float[]{0f, 0f}); - // Now switch to the recording screen + // Navigate to the Recording Screen where user will enter ID and click Start ((RecordingActivity) requireActivity()).showRecordingScreen(); - // If the Activity is ReplayActivity } else if (requireActivity() instanceof ReplayActivity) { - // *Do not* cast to RecordingActivity here // Just call the Replay method ((ReplayActivity) requireActivity()).onStartLocationChosen(chosenLat, chosenLon); - - // Otherwise (unexpected host) - } else { - // Optional: log or handle error - // Log.e("StartLocationFragment", "Unknown host Activity: " + requireActivity()); } } }); } - - /** - * 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/presentation/fragment/TrajectoryMapFragment.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/TrajectoryMapFragment.java index eb0bad65..5acc5402 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/TrajectoryMapFragment.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/TrajectoryMapFragment.java @@ -2,6 +2,7 @@ import android.graphics.Color; import android.os.Bundle; +import android.os.SystemClock; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -10,6 +11,9 @@ import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + import com.google.android.material.switchmaterial.SwitchMaterial; import androidx.annotation.NonNull; @@ -19,6 +23,8 @@ import com.google.android.gms.maps.OnMapReadyCallback; import com.openpositioning.PositionMe.R; import com.openpositioning.PositionMe.sensors.SensorFusion; +import com.openpositioning.PositionMe.utils.BuildingPolygon; +import com.openpositioning.PositionMe.utils.GeometryUtils; import com.openpositioning.PositionMe.utils.IndoorMapManager; import com.openpositioning.PositionMe.utils.UtilFunctions; import com.google.android.gms.maps.CameraUpdateFactory; @@ -29,58 +35,105 @@ import java.util.ArrayList; import java.util.List; - -/** - * A fragment responsible for displaying a trajectory map using Google Maps. - *

- * The TrajectoryMapFragment provides a map interface for visualizing movement trajectories, - * GNSS tracking, and indoor mapping. It manages map settings, user interactions, and real-time - * updates to user location and GNSS markers. - *

- * Key Features: - * - Displays a Google Map with support for different map types (Hybrid, Normal, Satellite). - * - Tracks and visualizes user movement using polylines. - * - Supports GNSS position updates and visual representation. - * - Includes indoor mapping with floor selection and auto-floor adjustments. - * - Allows user interaction through map controls and UI elements. - * - * @see com.openpositioning.PositionMe.presentation.activity.RecordingActivity The activity hosting this fragment. - * @see com.openpositioning.PositionMe.utils.IndoorMapManager Utility for managing indoor map overlays. - * @see com.openpositioning.PositionMe.utils.UtilFunctions Utility functions for UI and graphics handling. - * - * @author Mate Stodulka - */ - +// Map rendering fragment for live tracking and trajectory replay. +// It draws PDR, GNSS, and WiFi overlays, manages indoor map integration, +// and coordinates floor switching behavior for indoor navigation. public class TrajectoryMapFragment extends Fragment { - private GoogleMap gMap; // Google Maps instance - private LatLng currentLocation; // Stores the user's current location - private Marker orientationMarker; // Marker representing user's heading - private Marker gnssMarker; // GNSS position marker - private Polyline polyline; // Polyline representing user's movement path - private boolean isRed = true; // Tracks whether the polyline color is red - private boolean isGnssOn = false; // Tracks if GNSS tracking is enabled - - private Polyline gnssPolyline; // Polyline for GNSS path - private LatLng lastGnssLocation = null; // Stores the last GNSS location - - private LatLng pendingCameraPosition = null; // Stores pending camera movement - private boolean hasPendingCameraMove = false; // Tracks if camera needs to move + public interface OnVenueSelectedListener { + void onVenueSelected(String buildingId, String venueName); + } - private IndoorMapManager indoorMapManager; // Manages indoor mapping + private GoogleMap gMap; + private LatLng currentLocation; + private Marker orientationMarker; + private Marker gnssMarker; + private Marker wifiMarker; + private Polyline pdrPolyline; + private Polyline fusedPolyline; + private boolean isRed = true; + private boolean isGnssOn = false; + private boolean isWifiOn = false; + + private Polyline gnssPolyline; + private Polyline wifiPolyline; + private LatLng lastGnssLocation = null; + private LatLng lastWifiLocation = null; + + // Smoothing and motion constraints for stable map rendering. + private LatLng lastFusedSmoothedLocation = null; + private static final double FUSED_SMOOTH_ALPHA = 0.70; // 0-1.0 + private static final double DISPLAY_JITTER_DEADBAND_METERS = 1.0; + private static final double DISPLAY_MAX_JUMP_INDOOR_METERS = 3.0; + private static final double DISPLAY_MAX_JUMP_OUTDOOR_METERS = 3.0; + private static final double DISPLAY_SMOOTH_ALPHA_INDOOR = 0.30; + private static final double DISPLAY_SMOOTH_ALPHA_OUTDOOR = 0.28; + private static final double DISPLAY_MAX_WALKING_SPEED_MPS = 2.0; + private static final double DISPLAY_SHORT_WINDOW_MAX_METERS = 1.5; + private static final double DISPLAY_MIN_TIME_DELTA_SECONDS = 0.01; + + // Number of recent points retained for local visual history markers. + private static final int RECENT_POINTS_N = 5; + private final List recentCirclesBuffer = new ArrayList<>(); + + // GNSS and WiFi history markers retained on the map. + private static final int MAX_GNSS_WIFI_HISTORY = 5; + private final List gnssHistoryCircles = new ArrayList<>(); + private final List wifiHistoryCircles = new ArrayList<>(); + private LatLng lastGnssPositionForHistory = null; + private LatLng lastWifiPositionForHistory = null; + + private LatLng pendingCameraPosition = null; + private boolean hasPendingCameraMove = false; + private LatLng pendingReplayStartPosition = null; + private long lastDisplayFilterTimestampMs = -1L; + private Marker replayStartMarker; + + private IndoorMapManager indoorMapManager; private SensorFusion sensorFusion; - - - // UI + private OnVenueSelectedListener venueSelectedListener; + + private List manualMarkers = new ArrayList<>(); + + // Tracks whether the user marker is currently inside a known building. + private boolean isArrowInsideBuilding = false; + + // Calibration offset that maps fused floor estimates to building floor indices. + private static final int AUTO_FLOOR_REQUIRED_CONFIRMATIONS = 5; + private static final long AUTO_FLOOR_SWITCH_COOLDOWN_MS = 7000L; + // Barometric floor bands used by the auto-floor fallback path. + private static final float FLOOR_BAND_B1_MAX_METERS = 128.5f; + private static final float FLOOR_BAND_GF_MAX_METERS = 132.75f; + private static final float FLOOR_BAND_F1_MAX_METERS = 137.5f; + private static final float FLOOR_BAND_F2_MAX_METERS = 142.7f; + private int autoFloorOffset = 0; + private int pendingAutoFloorTarget = Integer.MIN_VALUE; + private int pendingAutoFloorCount = 0; + private long lastAutoFloorSwitchTimestampMs = 0L; + + // Map controls and floor UI elements. private Spinner switchMapSpinner; - private SwitchMaterial gnssSwitch; + private SwitchMaterial pdrSwitch; + private SwitchMaterial wifiSwitch; + private SwitchMaterial indoorMapSwitch; private SwitchMaterial autoFloorSwitch; - + private com.google.android.material.button.MaterialButton mapControlsToggleButton; private com.google.android.material.floatingactionbutton.FloatingActionButton floorUpButton, floorDownButton; private Button switchColorButton; - private Polygon buildingPolygon; - + private TextView floorTextView; + private View floorControlsContainer; + private View buildingInfoCard; + private TextView buildingNameText; + private View mapControlsContent; + private boolean suppressAutoFloorCallback = false; + private boolean mapControlsExpanded = false; + private boolean autoFloorProximityManaged = false; + private boolean wasNearVerticalTransition = false; + private boolean autoFloorArmedByTransitionEntry = false; + private boolean forceHardcodedBandsAfterStart = false; + private boolean isPdrOn = true; + private boolean pendingInitialFloorDetection = false; public TrajectoryMapFragment() { // Required empty public constructor @@ -91,7 +144,6 @@ public TrajectoryMapFragment() { public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - // Inflate the separate layout containing map + map-related UI return inflater.inflate(R.layout.fragment_trajectory_map, container, false); } @@ -100,150 +152,309 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - // Grab references to UI controls + sensorFusion = SensorFusion.getInstance(); + switchMapSpinner = view.findViewById(R.id.mapSwitchSpinner); + pdrSwitch = view.findViewById(R.id.pdrSwitch); gnssSwitch = view.findViewById(R.id.gnssSwitch); + wifiSwitch = view.findViewById(R.id.wifiSwitch); + indoorMapSwitch = view.findViewById(R.id.indoorMapSwitch); autoFloorSwitch = view.findViewById(R.id.autoFloor); + mapControlsToggleButton = view.findViewById(R.id.mapControlsToggleButton); floorUpButton = view.findViewById(R.id.floorUpButton); floorDownButton = view.findViewById(R.id.floorDownButton); switchColorButton = view.findViewById(R.id.lineColorButton); + floorTextView = view.findViewById(R.id.floorTextView); + floorControlsContainer = view.findViewById(R.id.floorControlsContainer); + buildingInfoCard = view.findViewById(R.id.buildingInfoCard); + buildingNameText = view.findViewById(R.id.buildingNameText); + mapControlsContent = view.findViewById(R.id.mapControlsContent); + + // Sync runtime flags with switch default state from XML. + if (pdrSwitch != null) { + isPdrOn = pdrSwitch.isChecked(); + } + if (gnssSwitch != null) { + isGnssOn = gnssSwitch.isChecked(); + } + if (wifiSwitch != null) { + isWifiOn = wifiSwitch.isChecked(); + } - // Setup floor up/down UI hidden initially until we know there's an indoor map setFloorControlsVisibility(View.GONE); + updateMapToggleState(); + applyMapControlsExpandedState(); + if (mapControlsToggleButton != null) { + mapControlsToggleButton.setOnClickListener(v -> { + mapControlsExpanded = !mapControlsExpanded; + applyMapControlsExpandedState(); + }); + } - // Initialize the map asynchronously SupportMapFragment mapFragment = (SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.trajectoryMap); if (mapFragment != null) { mapFragment.getMapAsync(new OnMapReadyCallback() { @Override public void onMapReady(@NonNull GoogleMap googleMap) { - // Assign the provided googleMap to your field variable gMap = googleMap; - // Initialize map settings with the now non-null gMap initMapSettings(gMap); - // If we had a pending camera move, apply it now + // Hybrid strategy: local fallback plus API loading. + + // Local fallback: immediately load Murchison/Nucleus/Library + if (indoorMapManager != null) { + indoorMapManager.addFallbackBuildings(); + } + + // Camera movement + LatLng kbCampus = new LatLng(55.9230, -3.1750); + if (!hasPendingCameraMove) { + gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(kbCampus, 17.5f)); + } + + // Click interaction + gMap.setOnPolygonClickListener(new GoogleMap.OnPolygonClickListener() { + @Override + public void onPolygonClick(@NonNull Polygon polygon) { + if (indoorMapManager != null) { + boolean handled = indoorMapManager.onPolygonClick(polygon); + if (handled) { + if (indoorMapSwitch != null && !indoorMapSwitch.isChecked()) { + indoorMapSwitch.setChecked(true); + } + // Don't show floor controls yet - wait for API callback + // Just show building name immediately + String venueName = indoorMapManager.getSelectedBuildingName(); + if (buildingInfoCard != null && buildingNameText != null && venueName != null) { + buildingNameText.setText(venueName); + buildingInfoCard.setVisibility(View.VISIBLE); + } + + updateMapToggleState(); + + // Venue selection notification will be sent in API callback + } + } + } + }); + if (hasPendingCameraMove && pendingCameraPosition != null) { gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(pendingCameraPosition, 19f)); + // Add a marker at the pending initial position + if (orientationMarker == null) { + orientationMarker = gMap.addMarker(new MarkerOptions() + .position(pendingCameraPosition) + .flat(true) + .anchor(0.5f, 0.5f) + .rotation(resolveMarkerHeadingDegrees(0f)) + .title("Start Position") + .icon(BitmapDescriptorFactory.fromBitmap( + UtilFunctions.getBitmapFromVector(requireContext(), + R.drawable.ic_baseline_navigation_24)))); + } + currentLocation = pendingCameraPosition; hasPendingCameraMove = false; pendingCameraPosition = null; } - drawBuildingPolygon(); - - Log.d("TrajectoryMapFragment", "onMapReady: Map is ready!"); - + if (pendingReplayStartPosition != null) { + setReplayStartMarker(pendingReplayStartPosition); + pendingReplayStartPosition = null; + } + updateMapToggleState(); + Log.d("TrajectoryMapFragment", "Map Ready: Hybrid Mode."); } }); } - // Map type spinner setup initMapTypeSpinner(); - // GNSS Switch + if (pdrSwitch != null) { + pdrSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { + isPdrOn = isChecked; + if (pdrPolyline != null) { + pdrPolyline.setVisible(isChecked); + } + if (switchColorButton != null) { + switchColorButton.setEnabled(isChecked); + switchColorButton.setAlpha(isChecked ? 1.0f : 0.45f); + } + }); + } + gnssSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { isGnssOn = isChecked; if (!isChecked && gnssMarker != null) { gnssMarker.remove(); gnssMarker = null; } + if (gnssPolyline != null) { + gnssPolyline.setPoints(new ArrayList<>()); + gnssPolyline.setVisible(false); + } else if (isChecked) { + // When GNSS is turned on, immediately show current GNSS position + if (sensorFusion != null && gMap != null) { + float[] gnssCoords = sensorFusion.getGNSSLatitude(false); + if (gnssCoords[0] != 0 || gnssCoords[1] != 0) { + LatLng gnssLocation = new LatLng(gnssCoords[0], gnssCoords[1]); + updateGNSS(gnssLocation); + } + } + } + }); + + if (wifiSwitch != null) { + wifiSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { + isWifiOn = isChecked; + if (wifiPolyline != null) { + wifiPolyline.setPoints(new ArrayList<>()); + wifiPolyline.setVisible(false); + } + if (!isChecked && wifiMarker != null) { + wifiMarker.remove(); + wifiMarker = null; + } else if (isChecked && sensorFusion != null && gMap != null) { + float[] wifiCoords = sensorFusion.getSensorValueMap().get(com.openpositioning.PositionMe.sensors.SensorTypes.WIFI); + if (wifiCoords != null && (wifiCoords[0] != 0 || wifiCoords[1] != 0)) { + updateWifi(new LatLng(wifiCoords[0], wifiCoords[1])); + } + } + }); + } + + indoorMapSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (indoorMapManager != null) { + indoorMapManager.setIndoorMapVisible(isChecked); + if (isChecked) { + ensureIndoorBuildingSelection(); + } + if (!isChecked) { + setAutoFloorChecked(false); + } + updateMapToggleState(); + } }); - // Color switch switchColorButton.setOnClickListener(v -> { - if (polyline != null) { + if (pdrPolyline != null) { if (isRed) { switchColorButton.setBackgroundColor(Color.BLACK); - polyline.setColor(Color.BLACK); + pdrPolyline.setColor(Color.BLACK); isRed = false; } else { switchColorButton.setBackgroundColor(Color.RED); - polyline.setColor(Color.RED); + pdrPolyline.setColor(Color.RED); isRed = true; } } }); - // Floor up/down logic autoFloorSwitch.setOnCheckedChangeListener((compoundButton, isChecked) -> { + if (suppressAutoFloorCallback) { + return; + } - //TODO - fix the sensor fusion method to get the elevation (cannot get it from the current method) -// float elevationVal = sensorFusion.getElevation(); -// indoorMapManager.setCurrentFloor((int)(elevationVal/indoorMapManager.getFloorHeight()) -// ,true); + if (!canUseAutoFloor()) { + if (isChecked) { + Toast.makeText(getContext(), "Turn on Indoor Map in a mapped building first", Toast.LENGTH_SHORT).show(); + setAutoFloorChecked(false); + } + return; + } + + if (isChecked && indoorMapManager != null && sensorFusion != null) { + syncIndoorFloorReference(); + pendingInitialFloorDetection = true; + performInitialFloorDetectionIfPending(); + + int currentManualFloor = indoorMapManager.getCurrentFloor(); + int estimatedFloor = resolveEstimatedFloorIndexForAutoMap(); + autoFloorOffset = currentManualFloor - estimatedFloor; + clearPendingAutoFloor(); + + Log.d("AutoFloor", "Enabled using fused floor estimate. Calibration Offset: " + autoFloorOffset); + Toast.makeText(getContext(), "Auto Floor Initialized", Toast.LENGTH_SHORT).show(); + } else { + autoFloorOffset = 0; + clearPendingAutoFloor(); + } }); floorUpButton.setOnClickListener(v -> { - // If user manually changes floor, turn off auto floor - autoFloorSwitch.setChecked(false); + setAutoFloorChecked(false); if (indoorMapManager != null) { indoorMapManager.increaseFloor(); + syncIndoorFloorReference(); + updateFloorDisplay(); } }); floorDownButton.setOnClickListener(v -> { - autoFloorSwitch.setChecked(false); + setAutoFloorChecked(false); if (indoorMapManager != null) { indoorMapManager.decreaseFloor(); + syncIndoorFloorReference(); + updateFloorDisplay(); } }); } - /** - * Initialize the map settings with the provided GoogleMap instance. - *

- * The method sets basic map settings, initializes the indoor map manager, - * and creates an empty polyline for user movement tracking. - * The method also initializes the GNSS polyline for tracking GNSS path. - * The method sets the map type to Hybrid and initializes the map with these settings. - * - * @param map - */ + public void addMarkerToMap(LatLng location) { + if (gMap != null && location != null) { + Marker marker = gMap.addMarker(new MarkerOptions() + .position(location) + .title("Manual Marker") + .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_YELLOW))); + if (marker != null) { + manualMarkers.add(marker); + } + } + } private void initMapSettings(GoogleMap map) { - // Basic map settings map.getUiSettings().setCompassEnabled(true); - map.getUiSettings().setTiltGesturesEnabled(true); - map.getUiSettings().setRotateGesturesEnabled(true); - map.getUiSettings().setScrollGesturesEnabled(true); map.setMapType(GoogleMap.MAP_TYPE_HYBRID); - // Initialize indoor manager - indoorMapManager = new IndoorMapManager(map); - - // Initialize an empty polyline - polyline = map.addPolyline(new PolylineOptions() - .color(Color.RED) - .width(5f) - .add() // start empty - ); + // Initialize Manager with Context + indoorMapManager = new IndoorMapManager(map, requireContext()); + indoorMapManager.setOnFloorDataLoadedListener((hasData) -> { + // Update floor display when floor data is loaded from API + if (getActivity() != null) { + getActivity().runOnUiThread(() -> { + if (hasData) { + setFloorControlsVisibility(View.VISIBLE); + syncIndoorFloorReference(); + updateFloorDisplay(); + updateMapToggleState(); + performInitialFloorDetectionIfPending(); + + // Now send venue selection notification + String buildingId = indoorMapManager.getSelectedBuildingId(); + String venueName = indoorMapManager.getSelectedBuildingName(); + if (venueSelectedListener != null && buildingId != null && venueName != null) { + venueSelectedListener.onVenueSelected(buildingId, venueName); + } + } else { + // No floor data for this building - hide floor controls + setFloorControlsVisibility(View.GONE); + setAutoFloorChecked(false); + updateMapToggleState(); + } + }); + } + }); - // GNSS path in blue - gnssPolyline = map.addPolyline(new PolylineOptions() - .color(Color.BLUE) - .width(5f) - .add() // start empty - ); + pdrPolyline = map.addPolyline(new PolylineOptions().color(Color.RED).width(5f)); + pdrPolyline.setVisible(isPdrOn); + fusedPolyline = map.addPolyline(new PolylineOptions().color(Color.GREEN).width(5f)); + gnssPolyline = map.addPolyline(new PolylineOptions().color(Color.BLUE).width(5f)); + gnssPolyline.setVisible(false); + wifiPolyline = map.addPolyline(new PolylineOptions().color(Color.rgb(255, 191, 0)).width(5f)); + wifiPolyline.setVisible(false); } - - /** - * Initialize the map type spinner with the available map types. - *

- * The spinner allows the user to switch between different map types - * (e.g. Hybrid, Normal, Satellite) to customize their map view. - * The spinner is populated with the available map types and listens - * for user selection to update the map accordingly. - * The map type is updated directly on the GoogleMap instance. - *

- * Note: The spinner is initialized with the default map type (Hybrid). - * The map type is updated on user selection. - *

- *

- * @see com.google.android.gms.maps.GoogleMap The GoogleMap instance to update map type. - */ private void initMapTypeSpinner() { if (switchMapSpinner == null) return; String[] maps = new String[]{ @@ -260,282 +471,736 @@ private void initMapTypeSpinner() { switchMapSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override - public void onItemSelected(AdapterView parent, View view, - int position, long id) { + public void onItemSelected(AdapterView parent, View view, int position, long id) { if (gMap == null) return; switch (position){ - case 0: - gMap.setMapType(GoogleMap.MAP_TYPE_HYBRID); - break; - case 1: - gMap.setMapType(GoogleMap.MAP_TYPE_NORMAL); - break; - case 2: - gMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE); - break; + case 0: gMap.setMapType(GoogleMap.MAP_TYPE_HYBRID); break; + case 1: gMap.setMapType(GoogleMap.MAP_TYPE_NORMAL); break; + case 2: gMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE); break; } } - @Override - public void onNothingSelected(AdapterView parent) {} + @Override public void onNothingSelected(AdapterView parent) {} }); } - /** - * Update the user's current location on the map, create or move orientation marker, - * and append to polyline if the user actually moved. - * - * @param newLocation The new location to plot. - * @param orientation The user’s heading (e.g. from sensor fusion). - */ public void updateUserLocation(@NonNull LatLng newLocation, float orientation) { if (gMap == null) return; - // Keep track of current location LatLng oldLocation = this.currentLocation; - this.currentLocation = newLocation; + LatLng constrainedLocation = newLocation; + + if (indoorMapManager != null && oldLocation != null && indoorMapManager.hasIndoorConstraints()) { + constrainedLocation = indoorMapManager.validatePosition(newLocation, oldLocation); + } + + LatLng displayLocation = filterDisplayLocation(constrainedLocation, oldLocation); + this.currentLocation = displayLocation; + + // Initialize PDR polyline if not exists (important for replay) + if (pdrPolyline == null) { + pdrPolyline = gMap.addPolyline(new PolylineOptions().color(Color.RED).width(5f)); + Log.d("TrajectoryMapFragment", "PDR polyline initialized in updateUserLocation"); + pdrPolyline.setVisible(isPdrOn); + } - // If no marker, create it if (orientationMarker == null) { orientationMarker = gMap.addMarker(new MarkerOptions() - .position(newLocation) + .position(displayLocation) .flat(true) + .anchor(0.5f, 0.5f) + .rotation(resolveMarkerHeadingDegrees(orientation)) .title("Current Position") .icon(BitmapDescriptorFactory.fromBitmap( UtilFunctions.getBitmapFromVector(requireContext(), R.drawable.ic_baseline_navigation_24))) ); - gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(newLocation, 19f)); + gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(displayLocation, 19f)); } else { - // Update marker position + orientation - orientationMarker.setPosition(newLocation); - orientationMarker.setRotation(orientation); - // Move camera a bit - gMap.moveCamera(CameraUpdateFactory.newLatLng(newLocation)); + orientationMarker.setPosition(displayLocation); + orientationMarker.setRotation(resolveMarkerHeadingDegrees(orientation)); + gMap.moveCamera(CameraUpdateFactory.newLatLng(displayLocation)); } - // Extend polyline if movement occurred - if (oldLocation != null && !oldLocation.equals(newLocation) && polyline != null) { - List points = new ArrayList<>(polyline.getPoints()); - points.add(newLocation); - polyline.setPoints(points); + // Add point to PDR trajectory polyline (red) + if (pdrPolyline == null) { + pdrPolyline = gMap.addPolyline(new PolylineOptions().color(Color.RED).width(4f)); + pdrPolyline.setVisible(isPdrOn); } + List pdrPoints = new ArrayList<>(pdrPolyline.getPoints()); + pdrPoints.add(displayLocation); + pdrPolyline.setPoints(pdrPoints); - // Update indoor map overlay - if (indoorMapManager != null) { - indoorMapManager.setCurrentLocation(newLocation); - setFloorControlsVisibility(indoorMapManager.getIsIndoorMapSet() ? View.VISIBLE : View.GONE); + // Mark absolute position updates on the map using a distinct color for PDR. + addRecentPoint(displayLocation, Color.RED); + + if (indoorMapManager != null) { + indoorMapManager.setCurrentLocation(displayLocation); + updateAutoFloorProximityState(displayLocation); + + // Auto-enable indoor map when the user marker enters a building. + // Check if the arrow is inside a building boundary + boolean isCurrentlyInsideBuilding = checkIfInsideBuilding(displayLocation); + + // If arrow just entered a building, auto-enable indoor map + if (isCurrentlyInsideBuilding && !isArrowInsideBuilding) { + Log.d("TrajectoryMapFragment", "Arrow entered building - auto-enabling indoor map"); + if (!indoorMapSwitch.isChecked()) { + indoorMapSwitch.setChecked(true); + } + updateMapToggleState(); + isArrowInsideBuilding = true; + } + // If arrow just exited a building + else if (!isCurrentlyInsideBuilding && isArrowInsideBuilding) { + Log.d("TrajectoryMapFragment", "Arrow exited building"); + isArrowInsideBuilding = false; + } + + if (autoFloorSwitch.isChecked() && indoorMapManager != null) { + try { + if (isCurrentlyInsideBuilding && indoorMapManager.getAvailableFloorsCount() > 0) { + boolean initialOrTransitionAllowed = pendingInitialFloorDetection || autoFloorArmedByTransitionEntry; + if (!initialOrTransitionAllowed) { + clearPendingAutoFloor(); + } else { + + int estimatedFloor = resolveEstimatedFloorIndexForAutoMap(); + int targetFloor = clampFloorIndex(estimatedFloor + autoFloorOffset); + int currentFloor = indoorMapManager.getCurrentFloor(); + + if (targetFloor != currentFloor) { + if (pendingAutoFloorTarget == targetFloor) { + pendingAutoFloorCount++; + } else { + pendingAutoFloorTarget = targetFloor; + pendingAutoFloorCount = 1; + } + + if (pendingAutoFloorCount >= AUTO_FLOOR_REQUIRED_CONFIRMATIONS) { + long nowMs = SystemClock.elapsedRealtime(); + if (nowMs - lastAutoFloorSwitchTimestampMs < AUTO_FLOOR_SWITCH_COOLDOWN_MS) { + clearPendingAutoFloor(); + } else { + Log.d("TrajectoryMapFragment", String.format( + "AutoFloor: Switching floor %d -> %d (estimated=%d, offset=%d)", + currentFloor, targetFloor, estimatedFloor, autoFloorOffset)); + indoorMapManager.setCurrentFloor(targetFloor, true); + lastAutoFloorSwitchTimestampMs = nowMs; + updateFloorDisplay(); + clearPendingAutoFloor(); + } + } + } else { + clearPendingAutoFloor(); + } + } + } + } catch (Exception e) { + Log.e("TrajectoryMapFragment", "AutoFloor error: " + e.getMessage(), e); + } + } + } + } + + private LatLng filterDisplayLocation(@NonNull LatLng candidate, @Nullable LatLng previous) { + long nowMs = SystemClock.elapsedRealtime(); + + if (previous == null) { + lastDisplayFilterTimestampMs = nowMs; + return candidate; + } + + double deltaSeconds = DISPLAY_MIN_TIME_DELTA_SECONDS; + if (lastDisplayFilterTimestampMs > 0) { + deltaSeconds = Math.max(DISPLAY_MIN_TIME_DELTA_SECONDS, + (nowMs - lastDisplayFilterTimestampMs) / 1000.0); + } + lastDisplayFilterTimestampMs = nowMs; + + double distanceMeters = GeometryUtils.distanceBetween(previous, candidate); + if (distanceMeters <= DISPLAY_JITTER_DEADBAND_METERS) { + return previous; + } + + boolean indoors = indoorMapManager != null && indoorMapManager.hasIndoorConstraints(); + double legacyMaxJumpMeters = indoors ? DISPLAY_MAX_JUMP_INDOOR_METERS : DISPLAY_MAX_JUMP_OUTDOOR_METERS; + double shortWindowSpeedLimitMeters = Math.max(0.6, + Math.min(DISPLAY_SHORT_WINDOW_MAX_METERS, DISPLAY_MAX_WALKING_SPEED_MPS * deltaSeconds)); + double maxJumpMeters = Math.min(legacyMaxJumpMeters, shortWindowSpeedLimitMeters); + LatLng limitedCandidate = candidate; + if (distanceMeters > maxJumpMeters) { + double ratio = maxJumpMeters / distanceMeters; + limitedCandidate = interpolate(previous, candidate, ratio); } + + double alpha = indoors ? DISPLAY_SMOOTH_ALPHA_INDOOR : DISPLAY_SMOOTH_ALPHA_OUTDOOR; + return interpolate(previous, limitedCandidate, alpha); + } + + private LatLng interpolate(@NonNull LatLng from, @NonNull LatLng to, double alpha) { + double clampedAlpha = Math.max(0.0, Math.min(1.0, alpha)); + double lat = from.latitude + (to.latitude - from.latitude) * clampedAlpha; + double lon = from.longitude + (to.longitude - from.longitude) * clampedAlpha; + return new LatLng(lat, lon); } + // Check if a location is inside any known building + // Uses building boundary polygons from BuildingPolygon class and dynamic boundaries from IndoorMapManager + private boolean checkIfInsideBuilding(LatLng location) { + if (indoorMapManager != null) { + return indoorMapManager.isLocationInsideAnyKnownBuilding(location); + } + + return BuildingPolygon.inAnyKnownBuilding(location); + } + // Public method to check if current location is inside a building + // Used by RecordingFragment for adaptive filtering + public boolean isCurrentlyInsideBuilding() { + return currentLocation != null && checkIfInsideBuilding(currentLocation); + } - /** - * Set the initial camera position for the map. - *

- * The method sets the initial camera position for the map when it is first loaded. - * If the map is already ready, the camera is moved immediately. - * If the map is not ready, the camera position is stored until the map is ready. - * The method also tracks if there is a pending camera move. - *

- * @param startLocation The initial camera position to set. - */ public void setInitialCameraPosition(@NonNull LatLng startLocation) { - // If the map is already ready, move camera immediately if (gMap != null) { gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(startLocation, 19f)); + // Add a marker at the initial position + if (orientationMarker == null) { + orientationMarker = gMap.addMarker(new MarkerOptions() + .position(startLocation) + .flat(true) + .anchor(0.5f, 0.5f) + .rotation(resolveMarkerHeadingDegrees(0f)) + .title("Start Position") + .icon(BitmapDescriptorFactory.fromBitmap( + UtilFunctions.getBitmapFromVector(requireContext(), + R.drawable.ic_baseline_navigation_24)))); + } + currentLocation = startLocation; } else { - // Otherwise, store it until onMapReady pendingCameraPosition = startLocation; hasPendingCameraMove = true; } } + public void setReplayStartMarker(@NonNull LatLng startLocation) { + if (gMap == null) { + pendingReplayStartPosition = startLocation; + return; + } + + if (replayStartMarker == null) { + replayStartMarker = gMap.addMarker(new MarkerOptions() + .position(startLocation) + .title("Recorded Start") + .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE))); + } else { + replayStartMarker.setPosition(startLocation); + } + } + + private float resolveMarkerHeadingDegrees(float orientationRadians) { + if (sensorFusion != null) { + return sensorFusion.getDisplayHeading(); + } + + if (!Float.isNaN(orientationRadians) && !Float.isInfinite(orientationRadians)) { + float degrees = (float) Math.toDegrees(orientationRadians); + float normalized = degrees % 360.0f; + return normalized < 0 ? normalized + 360.0f : normalized; + } + + return 0f; + } - /** - * Get the current user location on the map. - * @return The current user location as a LatLng object. - */ - public LatLng getCurrentLocation() { - return currentLocation; + public LatLng getCurrentLocation() { return currentLocation; } + public void setOnVenueSelectedListener(OnVenueSelectedListener listener) { + this.venueSelectedListener = listener; } - /** - * Called when we want to set or update the GNSS marker position - */ public void updateGNSS(@NonNull LatLng gnssLocation) { - if (gMap == null) return; - if (!isGnssOn) return; - - if (gnssMarker == null) { - // Create the GNSS marker for the first time - gnssMarker = gMap.addMarker(new MarkerOptions() - .position(gnssLocation) - .title("GNSS Position") - .icon(BitmapDescriptorFactory - .defaultMarker(BitmapDescriptorFactory.HUE_AZURE))); - lastGnssLocation = gnssLocation; - } else { - // Move existing GNSS marker - gnssMarker.setPosition(gnssLocation); - - // Add a segment to the blue GNSS line, if this is a new location - if (lastGnssLocation != null && !lastGnssLocation.equals(gnssLocation)) { - List gnssPoints = new ArrayList<>(gnssPolyline.getPoints()); - gnssPoints.add(gnssLocation); - gnssPolyline.setPoints(gnssPoints); + if (gMap == null || !isGnssOn) return; + + // GNSS rendering: keep only small history circles. + // Remove old WiFi pointer to avoid simultaneous display + if (wifiMarker != null) { + wifiMarker.remove(); + wifiMarker = null; + } + + // Remove GNSS large pointer if it exists + if (gnssMarker != null) { + gnssMarker.remove(); + gnssMarker = null; + } + + lastGnssLocation = gnssLocation; + + // Track GNSS position changes - add to history if position changed significantly + if (lastGnssPositionForHistory == null || + distanceBetweenLatLng(lastGnssPositionForHistory, gnssLocation) > 0.5) { + + // Draw a permanent small circle for GNSS history (Blue) + Circle circle = gMap.addCircle(new CircleOptions() + .center(gnssLocation) + .radius(0.4) // slightly larger than trajectory points + .fillColor(Color.BLUE) + .strokeColor(Color.BLACK) + .strokeWidth(1f) + .zIndex(90)); // Under trajectory but above map + + // Add to GNSS history + gnssHistoryCircles.add(circle); + if (gnssHistoryCircles.size() > MAX_GNSS_WIFI_HISTORY) { + Circle oldest = gnssHistoryCircles.remove(0); + if (oldest != null) oldest.remove(); } - lastGnssLocation = gnssLocation; + lastGnssPositionForHistory = gnssLocation; + } + + if (gnssPolyline != null) { + gnssPolyline.setPoints(new ArrayList<>()); + gnssPolyline.setVisible(false); } } + public void updateFused(@NonNull LatLng fusedLocation) { + if (gMap == null || fusedLocation == null) return; - /** - * Remove GNSS marker if user toggles it off - */ - public void clearGNSS() { + // Apply a simple smoothing filter to reduce sudden jump jitter. + LatLng smoothLocation = fusedLocation; + if (lastFusedSmoothedLocation != null) { + double alpha = FUSED_SMOOTH_ALPHA; + double smLat = alpha * fusedLocation.latitude + (1.0 - alpha) * lastFusedSmoothedLocation.latitude; + double smLon = alpha * fusedLocation.longitude + (1.0 - alpha) * lastFusedSmoothedLocation.longitude; + smoothLocation = new LatLng(smLat, smLon); + } + lastFusedSmoothedLocation = smoothLocation; + + // Initialize fused polyline if not exists + if (fusedPolyline == null) { + fusedPolyline = gMap.addPolyline(new PolylineOptions().color(Color.GREEN).width(5f)); + Log.d("TrajectoryMapFragment", "Fused Polyline initialized in updateFused"); + } + + List fusedPoints = new ArrayList<>(fusedPolyline.getPoints()); + fusedPoints.add(smoothLocation); + fusedPolyline.setPoints(fusedPoints); + + addRecentPoint(smoothLocation, Color.GREEN); + } + + public void updateWifi(@NonNull LatLng wifiLocation) { + if (gMap == null || wifiLocation == null || !isWifiOn) return; + + if (wifiPolyline == null) { + wifiPolyline = gMap.addPolyline(new PolylineOptions().color(Color.rgb(255, 191, 0)).width(5f)); + } + + // WiFi rendering: keep only small history circles. + // Remove old GNSS pointer to avoid simultaneous display if (gnssMarker != null) { gnssMarker.remove(); gnssMarker = null; } + + // Remove WiFi large pointer if it exists + if (wifiMarker != null) { + wifiMarker.remove(); + wifiMarker = null; + } + + lastWifiLocation = wifiLocation; + + // Always record WiFi history point on each update (no movement threshold). + Circle circle = gMap.addCircle(new CircleOptions() + .center(wifiLocation) + .radius(0.4) // slightly larger than trajectory points + .fillColor(Color.YELLOW) + .strokeColor(Color.BLACK) + .strokeWidth(1f) + .zIndex(90)); // Under trajectory but above map + + // Add to WiFi history + wifiHistoryCircles.add(circle); + if (wifiHistoryCircles.size() > MAX_GNSS_WIFI_HISTORY) { + Circle oldest = wifiHistoryCircles.remove(0); + if (oldest != null) oldest.remove(); + } + lastWifiPositionForHistory = wifiLocation; + + if (wifiPolyline != null) { + wifiPolyline.setPoints(new ArrayList<>()); + wifiPolyline.setVisible(false); + } + } + + private void addRecentPoint(LatLng position, int color) { + if (position == null || gMap == null) return; + + if (recentCirclesBuffer.size() >= RECENT_POINTS_N) { + Circle oldest = recentCirclesBuffer.remove(0); + if (oldest != null) { + oldest.remove(); + } + } + + Circle circle = gMap.addCircle(new CircleOptions() + .center(position) + .radius(0.3) // 3 meters radius + .fillColor(color) + .strokeColor(Color.BLACK) + .strokeWidth(1f) + .zIndex(100)); // Ensure it's drawn on top + + recentCirclesBuffer.add(circle); + } + + // Calculate distance between two LatLng points in meters using Haversine formula + private double distanceBetweenLatLng(LatLng p1, LatLng p2) { + if (p1 == null || p2 == null) return Double.MAX_VALUE; + + final int R = 6371000; // Earth's radius in meters + double lat1 = Math.toRadians(p1.latitude); + double lat2 = Math.toRadians(p2.latitude); + double deltaLat = Math.toRadians(p2.latitude - p1.latitude); + double deltaLon = Math.toRadians(p2.longitude - p1.longitude); + + double a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) + + Math.cos(lat1) * Math.cos(lat2) + * Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2); + double c = 2 * Math.asin(Math.sqrt(a)); + + return R * c; // distance in meters + } + + private float getHueFromColor(int color) { + if (color == Color.BLUE) return BitmapDescriptorFactory.HUE_AZURE; + if (color == Color.GREEN) return BitmapDescriptorFactory.HUE_GREEN; + if (color == Color.YELLOW) return BitmapDescriptorFactory.HUE_ORANGE; + if (color == Color.RED) return BitmapDescriptorFactory.HUE_RED; + return BitmapDescriptorFactory.HUE_VIOLET; } - /** - * Whether user is currently showing GNSS or not - */ - public boolean isGnssEnabled() { - return isGnssOn; + public void clearGNSS() { + if (gnssMarker != null) { + gnssMarker.remove(); + gnssMarker = null; + } } + public boolean isGnssEnabled() { return isGnssOn; } + private void setFloorControlsVisibility(int visibility) { - floorUpButton.setVisibility(visibility); - floorDownButton.setVisibility(visibility); - autoFloorSwitch.setVisibility(visibility); + if (floorControlsContainer != null) { + floorControlsContainer.setVisibility(visibility); + } + if (autoFloorSwitch != null) { + autoFloorSwitch.setVisibility(View.VISIBLE); + } } - public void clearMapAndReset() { - if (polyline != null) { - polyline.remove(); - polyline = null; + private void ensureIndoorBuildingSelection() { + if (indoorMapManager == null || gMap == null || !indoorMapManager.isIndoorMapVisible()) { + return; } - if (gnssPolyline != null) { - gnssPolyline.remove(); - gnssPolyline = null; + + LatLng anchorLocation = currentLocation; + if (anchorLocation == null && gMap.getCameraPosition() != null) { + anchorLocation = gMap.getCameraPosition().target; } - if (orientationMarker != null) { - orientationMarker.remove(); - orientationMarker = null; + + boolean selected = indoorMapManager.selectBuildingForLocation(anchorLocation, true); + if (selected && buildingInfoCard != null && buildingNameText != null) { + String venueName = indoorMapManager.getSelectedBuildingName(); + if (venueName != null) { + buildingNameText.setText(venueName); + buildingInfoCard.setVisibility(View.VISIBLE); + } } - if (gnssMarker != null) { - gnssMarker.remove(); - gnssMarker = null; + } + + public void detectCurrentFloorOnce() { + pendingInitialFloorDetection = true; + forceHardcodedBandsAfterStart = true; + + if (indoorMapManager == null) { + return; } - lastGnssLocation = null; - currentLocation = null; - // Re-create empty polylines with your chosen colors - if (gMap != null) { - polyline = gMap.addPolyline(new PolylineOptions() - .color(Color.RED) - .width(5f) - .add()); - gnssPolyline = gMap.addPolyline(new PolylineOptions() - .color(Color.BLUE) - .width(5f) - .add()); - } - } - - /** - * Draw the building polygon on the map - *

- * The method draws a polygon representing the building on the map. - * The polygon is drawn with specific vertices and colors to represent - * different buildings or areas on the map. - * The method removes the old polygon if it exists and adds the new polygon - * to the map with the specified options. - * The method logs the number of vertices in the polygon for debugging. - *

- * - * Note: The method uses hard-coded vertices for the building polygon. - * - *

- * - * See: {@link com.google.android.gms.maps.model.PolygonOptions} The options for the new polygon. - */ - private void drawBuildingPolygon() { - if (gMap == null) { - Log.e("TrajectoryMapFragment", "GoogleMap is not ready"); + if (!indoorMapManager.isIndoorMapVisible() && indoorMapSwitch != null) { + indoorMapSwitch.setChecked(true); + return; + } + + ensureIndoorBuildingSelection(); + performInitialFloorDetectionIfPending(); + } + + private void applyMapControlsExpandedState() { + if (mapControlsContent != null) { + mapControlsContent.setVisibility(mapControlsExpanded ? View.VISIBLE : View.GONE); + } + if (mapControlsToggleButton != null) { + mapControlsToggleButton.setIconResource( + mapControlsExpanded ? android.R.drawable.arrow_up_float : android.R.drawable.arrow_down_float + ); + } + } + + private void syncIndoorFloorReference() { + if (indoorMapManager == null || sensorFusion == null) { + return; + } + + if (indoorMapManager.getAvailableFloorsCount() <= 0) { + return; + } + + sensorFusion.setIndoorFloorReference( + indoorMapManager.getFloorHeight(), + indoorMapManager.getCurrentFloor(), + indoorMapManager.getFloorAltitudeAnchors() + ); + sensorFusion.setIndoorEnvironmentFeatures( + indoorMapManager.getCurrentFloorStairsZones(), + indoorMapManager.getCurrentFloorLiftZones(), + indoorMapManager.getCurrentFloorWalls() + ); + } + + private void performInitialFloorDetectionIfPending() { + if (!pendingInitialFloorDetection || indoorMapManager == null || sensorFusion == null) { + return; + } + + if (!canUseAutoFloor()) { return; } - // nuclear building polygon vertices - LatLng nucleus1 = new LatLng(55.92279538827796, -3.174612147506538); - LatLng nucleus2 = new LatLng(55.92278121423647, -3.174107900816096); - LatLng nucleus3 = new LatLng(55.92288405733954, -3.173843694667146); - LatLng nucleus4 = new LatLng(55.92331786793876, -3.173832892645086); - LatLng nucleus5 = new LatLng(55.923337194112555, -3.1746284301397387); + // Initial floor lock must use barometer bands even before entering stairs/lift zones. + sensorFusion.setBarometerAutoFloorEnabled(true); + int estimatedFloor = resolveEstimatedFloorIndexForAutoMap(); + indoorMapManager.setCurrentFloor(estimatedFloor, true); + syncIndoorFloorReference(); + updateFloorDisplay(); + if (!autoFloorArmedByTransitionEntry) { + sensorFusion.setBarometerAutoFloorEnabled(false); + } + pendingInitialFloorDetection = false; + } + private int resolveEstimatedFloorIndexForAutoMap() { + if (indoorMapManager == null || sensorFusion == null) { + return 0; + } - // nkml building polygon vertices - LatLng nkml1 = new LatLng(55.9230343434213, -3.1751847990731954); - LatLng nkml2 = new LatLng(55.923032840563366, -3.174777103346131); - LatLng nkml4 = new LatLng(55.92280139974615, -3.175195527934348); - LatLng nkml3 = new LatLng(55.922793885410734, -3.1747958788136867); + int bandFloor; + float altitudeMeters = sensorFusion.getEstimatedAbsoluteAltitude(); + if (forceHardcodedBandsAfterStart && !Float.isNaN(altitudeMeters)) { + bandFloor = getHardcodedBandFloorFromAltitude(altitudeMeters); + } else { + bandFloor = sensorFusion.getEstimatedFloorByBarometerBands(); + } - LatLng fjb1 = new LatLng(55.92269205199916, -3.1729563477188774);//left top - LatLng fjb2 = new LatLng(55.922822801570994, -3.172594249522305); - LatLng fjb3 = new LatLng(55.92223512226413, -3.171921917547244); - LatLng fjb4 = new LatLng(55.9221071265519, -3.1722813131202097); + return indoorMapManager.mapBarometerBandFloorToFloorIndex(bandFloor); + } - LatLng faraday1 = new LatLng(55.92242866264128, -3.1719553662011815); - LatLng faraday2 = new LatLng(55.9224966752294, -3.1717846714743474); - LatLng faraday3 = new LatLng(55.922271383074154, -3.1715191463437162); - LatLng faraday4 = new LatLng(55.92220124468304, -3.171705013935158); + private int getHardcodedBandFloorFromAltitude(float altitudeMeters) { + if (altitudeMeters <= FLOOR_BAND_B1_MAX_METERS) { + return 0; // B1 + } + if (altitudeMeters < FLOOR_BAND_GF_MAX_METERS) { + return 1; // GF + } + if (altitudeMeters < FLOOR_BAND_F1_MAX_METERS) { + return 2; // F1 + } + if (altitudeMeters < FLOOR_BAND_F2_MAX_METERS) { + return 3; // F2 + } + return 4; // F3+ + } + private boolean canUseAutoFloor() { + return indoorMapManager != null + && indoorMapManager.isIndoorMapVisible() + && indoorMapManager.getAvailableFloorsCount() > 0; + } + private void updateMapToggleState() { + if (autoFloorSwitch == null) { + return; + } - PolygonOptions buildingPolygonOptions = new PolygonOptions() - .add(nucleus1, nucleus2, nucleus3, nucleus4, nucleus5) - .strokeColor(Color.RED) // Red border - .strokeWidth(10f) // Border width - //.fillColor(Color.argb(50, 255, 0, 0)) // Semi-transparent red fill - .zIndex(1); // Set a higher zIndex to ensure it appears above other overlays + boolean autoFloorAvailable = canUseAutoFloor(); + autoFloorSwitch.setEnabled(autoFloorAvailable); + autoFloorSwitch.setAlpha(autoFloorAvailable ? 1.0f : 0.45f); - // Options for the new polygon - PolygonOptions buildingPolygonOptions2 = new PolygonOptions() - .add(nkml1, nkml2, nkml3, nkml4, nkml1) - .strokeColor(Color.BLUE) // Blue border - .strokeWidth(10f) // Border width - // .fillColor(Color.argb(50, 0, 0, 255)) // Semi-transparent blue fill - .zIndex(1); // Set a higher zIndex to ensure it appears above other overlays + if (!autoFloorAvailable && autoFloorSwitch.isChecked()) { + setAutoFloorChecked(false); + } - PolygonOptions buildingPolygonOptions3 = new PolygonOptions() - .add(fjb1, fjb2, fjb3, fjb4, fjb1) - .strokeColor(Color.GREEN) // Green border - .strokeWidth(10f) // Border width - //.fillColor(Color.argb(50, 0, 255, 0)) // Semi-transparent green fill - .zIndex(1); // Set a higher zIndex to ensure it appears above other overlays + if (floorControlsContainer != null) { + floorControlsContainer.setVisibility(autoFloorAvailable ? View.VISIBLE : View.GONE); + } + } - PolygonOptions buildingPolygonOptions4 = new PolygonOptions() - .add(faraday1, faraday2, faraday3, faraday4, faraday1) - .strokeColor(Color.YELLOW) // Yellow border - .strokeWidth(10f) // Border width - //.fillColor(Color.argb(50, 255, 255, 0)) // Semi-transparent yellow fill - .zIndex(1); // Set a higher zIndex to ensure it appears above other overlays + private void setAutoFloorChecked(boolean checked) { + if (autoFloorSwitch == null) { + return; + } + suppressAutoFloorCallback = true; + autoFloorSwitch.setChecked(checked); + suppressAutoFloorCallback = false; - // Remove the old polygon if it exists - if (buildingPolygon != null) { - buildingPolygon.remove(); + if (!checked) { + autoFloorOffset = 0; + clearPendingAutoFloor(); } + } - // Add the polygon to the map - buildingPolygon = gMap.addPolygon(buildingPolygonOptions); - gMap.addPolygon(buildingPolygonOptions2); - gMap.addPolygon(buildingPolygonOptions3); - gMap.addPolygon(buildingPolygonOptions4); - Log.d("TrajectoryMapFragment", "Building polygon added, vertex count: " + buildingPolygon.getPoints().size()); + private void clearPendingAutoFloor() { + pendingAutoFloorTarget = Integer.MIN_VALUE; + pendingAutoFloorCount = 0; } + private void updateAutoFloorProximityState(@NonNull LatLng location) { + if (autoFloorSwitch == null || indoorMapManager == null) { + return; + } + + boolean canAutoFloor = canUseAutoFloor(); + boolean nearVerticalTransition = canAutoFloor + && indoorMapManager.isNearCurrentFloorVerticalTransition(location); + if (!canAutoFloor) { + wasNearVerticalTransition = false; + autoFloorArmedByTransitionEntry = false; + if (sensorFusion != null) { + sensorFusion.setBarometerAutoFloorEnabled(false); + } + } else if (nearVerticalTransition && !wasNearVerticalTransition) { + autoFloorArmedByTransitionEntry = true; + if (sensorFusion != null) { + sensorFusion.setBarometerAutoFloorEnabled(true); + } + } else if (!nearVerticalTransition && wasNearVerticalTransition) { + autoFloorArmedByTransitionEntry = false; + if (sensorFusion != null) { + sensorFusion.setBarometerAutoFloorEnabled(false); + } + clearPendingAutoFloor(); + } + wasNearVerticalTransition = nearVerticalTransition; + + if (nearVerticalTransition) { + if (!autoFloorSwitch.isChecked()) { + autoFloorProximityManaged = true; + setAutoFloorChecked(true); + } + } else if (autoFloorProximityManaged && autoFloorSwitch.isChecked()) { + setAutoFloorChecked(false); + autoFloorProximityManaged = false; + } else if (!autoFloorSwitch.isChecked()) { + autoFloorProximityManaged = false; + } + } + + // Update floor display text based on current floor + private void updateFloorDisplay() { + if (indoorMapManager == null || floorTextView == null) return; + + int currentFloor = indoorMapManager.getCurrentFloor(); + String currentFloorName = indoorMapManager.getCurrentFloorName(); + int totalFloors = indoorMapManager.getAvailableFloorsCount(); + + String displayText; + if (currentFloorName != null && !currentFloorName.isEmpty()) { + displayText = currentFloorName; + } else if (currentFloor == 0) { + displayText = "G"; + } else if (currentFloor > 0) { + displayText = "F" + currentFloor; + } else { + displayText = "B" + Math.abs(currentFloor); + } + + if (totalFloors > 1) { + displayText += "\n" + (currentFloor + 1) + "/" + totalFloors; + } + + floorTextView.setText(displayText); + } + + private int clampFloorIndex(int floorIndex) { + int totalFloors = indoorMapManager != null ? indoorMapManager.getAvailableFloorsCount() : 0; + if (totalFloors <= 0) { + return floorIndex; + } + return Math.max(0, Math.min(floorIndex, totalFloors - 1)); + } + + public void clearMapAndReset() { + if (pdrPolyline != null) { pdrPolyline.remove(); pdrPolyline = null; } + if (fusedPolyline != null) { fusedPolyline.remove(); fusedPolyline = null; } + if (gnssPolyline != null) { gnssPolyline.remove(); gnssPolyline = null; } + if (wifiPolyline != null) { wifiPolyline.remove(); wifiPolyline = null; } + if (orientationMarker != null) { orientationMarker.remove(); orientationMarker = null; } + if (gnssMarker != null) { gnssMarker.remove(); gnssMarker = null; } + if (wifiMarker != null) { wifiMarker.remove(); wifiMarker = null; } + + for (Marker m : manualMarkers) m.remove(); + manualMarkers.clear(); + + for (Circle c : recentCirclesBuffer) { + if (c != null) c.remove(); + } + recentCirclesBuffer.clear(); + + // Clear GNSS/WiFi history buffers. + for (Circle c : gnssHistoryCircles) { + if (c != null) c.remove(); + } + gnssHistoryCircles.clear(); + + for (Circle c : wifiHistoryCircles) { + if (c != null) c.remove(); + } + wifiHistoryCircles.clear(); + + lastGnssPositionForHistory = null; + lastWifiPositionForHistory = null; + + lastGnssLocation = null; + lastWifiLocation = null; + currentLocation = null; + lastDisplayFilterTimestampMs = -1L; + + if (gMap != null) { + pdrPolyline = gMap.addPolyline(new PolylineOptions().color(Color.RED).width(5f)); + pdrPolyline.setVisible(isPdrOn); + fusedPolyline = gMap.addPolyline(new PolylineOptions().color(Color.GREEN).width(5f)); + gnssPolyline = gMap.addPolyline(new PolylineOptions().color(Color.BLUE).width(5f)); + gnssPolyline.setVisible(false); + wifiPolyline = gMap.addPolyline(new PolylineOptions().color(Color.rgb(255, 191, 0)).width(5f)); + wifiPolyline.setVisible(isWifiOn); + } + + if (indoorMapManager != null) { + indoorMapManager.hideMap(); + setFloorControlsVisibility(View.GONE); + if (buildingInfoCard != null) { + buildingInfoCard.setVisibility(View.GONE); + } + } + } + public LatLng getCameraTarget() { + if (gMap != null) { + return gMap.getCameraPosition().target; + } + return null; + } } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/UploadFragment.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/UploadFragment.java index 9d435812..79a8107a 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/UploadFragment.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/fragment/UploadFragment.java @@ -1,16 +1,19 @@ package com.openpositioning.PositionMe.presentation.fragment; +import android.content.SharedPreferences; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import android.widget.Toast; import android.os.Environment; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -21,18 +24,15 @@ import com.openpositioning.PositionMe.presentation.viewitems.UploadListAdapter; import java.io.File; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; -/** - * A simple {@link Fragment} subclass. Displays trajectories that were saved locally because no - * acceptable network was available to upload it when the recording finished. Trajectories can be - * uploaded manually. - * - * @author Mate Stodulka - */ +// A simple {@link Fragment} subclass. Displays trajectories that were saved locally. +// Now correctly detects files starting with "traj_" +// @author Mate Stodulka public class UploadFragment extends Fragment { // UI elements @@ -42,29 +42,21 @@ public class UploadFragment extends Fragment { // Server communication class private ServerCommunications serverCommunications; + private SharedPreferences settings; // List of files saved locally private List localTrajectories; - /** - * Public default constructor, empty. - */ public UploadFragment() { // Required empty public constructor } - - /** - * {@inheritDoc} - * Initialises new Server Communication instance with the context, and finds all the files that - * match the trajectory naming scheme in local storage. - */ - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Get communication class serverCommunications = new ServerCommunications(getActivity()); + settings = PreferenceManager.getDefaultSharedPreferences(getActivity()); // Determine the directory to load trajectory files from. File trajectoriesDir = null; @@ -79,43 +71,33 @@ public void onCreate(Bundle savedInstanceState) { trajectoriesDir = getActivity().getFilesDir(); } - localTrajectories = Stream.of(trajectoriesDir.listFiles((file, name) -> - name.contains("trajectory_") && name.endsWith(".txt"))) - .filter(file -> !file.isDirectory()) - .collect(Collectors.toList()); + // Filter trajectory files (both old and new formats) + File[] files = trajectoriesDir.listFiles((file, name) -> + name.startsWith("traj_") && name.endsWith(".txt")); + + if (files != null) { + localTrajectories = Stream.of(files) + .filter(file -> !file.isDirectory()) + .collect(Collectors.toList()); + } else { + localTrajectories = new ArrayList<>(); + } } - /** - * {@inheritDoc} - * Sets the title in the action bar to "Upload" - */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { getActivity().setTitle("Upload"); - // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_upload, container, false); } - /** - * {@inheritDoc} - * Checks if there are locally saved trajectories. If there are none, it displays a text message - * notifying the user. If there are local files, the text is hidden, and instead a Recycler View - * is displayed showing all the trajectories. - *

- * A Layout Manager is registered, and the adapter and list of files passed. An onClick listener - * is set up to upload the file when clicked and remove it from local storage. - * - * @see UploadListAdapter list adapter for the recycler view. - * @see UploadViewHolder view holder for the recycler view. - * @see com.openpositioning.PositionMe.R.layout#item_upload_card_view xml view for list elements. - */ @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); this.emptyNotice = view.findViewById(R.id.emptyUpload); this.uploadList = view.findViewById(R.id.uploadTrajectories); + // Check if there are locally saved trajectories if(localTrajectories.isEmpty()) { uploadList.setVisibility(View.GONE); @@ -129,20 +111,30 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat LinearLayoutManager manager = new LinearLayoutManager(getActivity()); uploadList.setLayoutManager(manager); uploadList.setHasFixedSize(true); + listAdapter = new UploadListAdapter(getActivity(), localTrajectories, new DownloadClickListener() { - /** - * {@inheritDoc} - * Upload the trajectory at the clicked position, remove it from the recycler view - * and the local list. - */ @Override public void onPositionClicked(int position) { - serverCommunications.uploadLocalTrajectory(localTrajectories.get(position)); -// localTrajectories.remove(position); -// listAdapter.notifyItemRemoved(position); + File selectedFile = localTrajectories.get(position); + if (!selectedFile.exists()) { + Toast.makeText(getActivity(), "File no longer exists: " + selectedFile.getName(), Toast.LENGTH_LONG).show(); + localTrajectories.remove(position); + listAdapter.notifyItemRemoved(position); + listAdapter.notifyItemRangeChanged(position, localTrajectories.size()); + if (localTrajectories.isEmpty()) { + uploadList.setVisibility(View.GONE); + emptyNotice.setVisibility(View.VISIBLE); + } + return; + } + + // Read campaign from SharedPreferences, default to empty string + String campaign = settings.getString("current_campaign", ""); + serverCommunications.uploadLocalTrajectory(selectedFile, campaign); } }); uploadList.setAdapter(listAdapter); } } -} \ No newline at end of file +} + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/BleListAdapter.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/BleListAdapter.java new file mode 100644 index 00000000..a8ea7af5 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/BleListAdapter.java @@ -0,0 +1,64 @@ +package com.openpositioning.PositionMe.presentation.viewitems; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.openpositioning.PositionMe.R; +import com.openpositioning.PositionMe.sensors.BleDevice; + +import java.util.List; + +// Adapter for displaying BLE device data in a RecyclerView. +// Related: BleViewHolder +// Related: com.openpositioning.PositionMe.sensors.BleDevice +public class BleListAdapter extends RecyclerView.Adapter { + + Context context; + List items; + + public BleListAdapter(Context context, List items) { + this.context = context; + this.items = items; + } + + @NonNull + @Override + public BleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new BleViewHolder(LayoutInflater.from(context).inflate(R.layout.item_ble_card_view, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull BleViewHolder holder, int position) { + BleDevice device = items.get(position); + + // Check if this is a placeholder item + if ("00:00:00:00:00:00".equals(device.getMacAddress())) { + holder.deviceName.setText(device.getName() != null ? device.getName() : "Scanning..."); + holder.macAddress.setText("No devices found"); + holder.rssi.setText(""); + } else { + holder.macAddress.setText(device.getMacAddress()); + + String name = device.getName(); + if (name == null || name.isEmpty()) { + holder.deviceName.setText("Unknown Device"); + } else { + holder.deviceName.setText(name); + } + + String rssiString = device.getRssi() + " dBm"; + holder.rssi.setText(rssiString); + } + } + + @Override + public int getItemCount() { + return items.size(); + } +} + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/BleViewHolder.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/BleViewHolder.java new file mode 100644 index 00000000..0d9f2b0b --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/BleViewHolder.java @@ -0,0 +1,28 @@ +package com.openpositioning.PositionMe.presentation.viewitems; + +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.openpositioning.PositionMe.R; + +// View holder for BLE device list items. +// Related: BleListAdapter +// Related: com.openpositioning.PositionMe.sensors.BleDevice +public class BleViewHolder extends RecyclerView.ViewHolder { + + TextView macAddress; + TextView deviceName; + TextView rssi; + + public BleViewHolder(@NonNull View itemView) { + super(itemView); + macAddress = itemView.findViewById(R.id.bleMacItem); + deviceName = itemView.findViewById(R.id.bleNameItem); + rssi = itemView.findViewById(R.id.bleRssiItem); + } +} + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/DownloadClickListener.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/DownloadClickListener.java index 202f4a43..fbb2d8fb 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/DownloadClickListener.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/DownloadClickListener.java @@ -1,17 +1,13 @@ package com.openpositioning.PositionMe.presentation.viewitems; -/** - * Interface to enable listening for clicks in RecyclerViews. - * - * @author Mate Stodulka - */ +// Interface to enable listening for clicks in RecyclerViews. +// @author Mate Stodulka public interface DownloadClickListener { - /** - * Function executed when the item is clicked. - * - * @param position integer position of the item in the list. - */ + // Function executed when the item is clicked. +// Parameter position: integer position of the item in the list. void onPositionClicked(int position); } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/SensorInfoListAdapter.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/SensorInfoListAdapter.java index 4315328d..3f5cfc18 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/SensorInfoListAdapter.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/SensorInfoListAdapter.java @@ -13,50 +13,37 @@ import java.util.List; import java.util.Objects; -/** - * Adapter used for displaying sensor info data. - * - * @see SensorInfoViewHolder corresponding View Holder class - * @see com.openpositioning.PositionMe.R.layout#item_sensorinfo_card_view xml layout file - * - * @author Mate Stodulka - */ +// Adapter used for displaying sensor info data. +// Related: SensorInfoViewHolder corresponding View Holder class +// Related: com.openpositioning.PositionMe.R.layout#item_sensorinfo_card_view xml layout file +// @author Mate Stodulka public class SensorInfoListAdapter extends RecyclerView.Adapter { Context context; List sensorInfoList; - /** - * Default public constructor with context for inflating views and list to be displayed. - * - * @param context application context to enable inflating views used in the list. - * @param sensorInfoList list of SensorInfo objects to be displayed in the list. - * - * @see SensorInfo the data class. - */ + // Default public constructor with context for inflating views and list to be displayed. +// Parameter context: application context to enable inflating views used in the list. +// Parameter sensorInfoList: list of SensorInfo objects to be displayed in the list. +// Related: SensorInfo the data class. public SensorInfoListAdapter(Context context, List sensorInfoList) { this.context = context; this.sensorInfoList = sensorInfoList; } - /** - * {@inheritDoc} - * @see com.openpositioning.PositionMe.R.layout#item_sensorinfo_card_view xml layout file - */ + // {@inheritDoc} +// Related: com.openpositioning.PositionMe.R.layout#item_sensorinfo_card_view xml layout file @NonNull @Override public SensorInfoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new SensorInfoViewHolder(LayoutInflater.from(context).inflate(R.layout.item_sensorinfo_card_view, parent, false)); } - /** - * {@inheritDoc} - * Formats and assigns the data fields from the SensorInfo object to the TextView fields. - * - * @see SensorInfo data class - * @see com.openpositioning.PositionMe.R.string formatting for strings. - * @see com.openpositioning.PositionMe.R.layout#item_sensorinfo_card_view xml layout file - */ + // {@inheritDoc} + // Formats and assigns the data fields from the SensorInfo object to the TextView fields. +// Related: SensorInfo data class +// Related: com.openpositioning.PositionMe.R.string formatting for strings. +// Related: com.openpositioning.PositionMe.R.layout#item_sensorinfo_card_view xml layout file @Override public void onBindViewHolder(@NonNull SensorInfoViewHolder holder, int position) { holder.name.setText(sensorInfoList.get(position).getName()); @@ -72,14 +59,13 @@ public void onBindViewHolder(@NonNull SensorInfoViewHolder holder, int position) holder.version.setText(versionString); } - /** - * {@inheritDoc} - * Number of SensorInfo objects. - * - * @see SensorInfo - */ + // {@inheritDoc} + // Number of SensorInfo objects. +// Related: SensorInfo @Override public int getItemCount() { return sensorInfoList.size(); } } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/SensorInfoViewHolder.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/SensorInfoViewHolder.java index 469ec16e..08c24914 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/SensorInfoViewHolder.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/SensorInfoViewHolder.java @@ -8,25 +8,18 @@ import com.openpositioning.PositionMe.R; -/** - * View holder class for the RecyclerView displaying SensorInfo data. - * - * @see SensorInfoListAdapter the corresponding list adapter. - * @see com.openpositioning.PositionMe.R.layout#item_sensorinfo_card_view xml layout file - * - * @author Mate Stodulka - */ +// View holder class for the RecyclerView displaying SensorInfo data. +// Related: SensorInfoListAdapter the corresponding list adapter. +// Related: com.openpositioning.PositionMe.R.layout#item_sensorinfo_card_view xml layout file +// @author Mate Stodulka public class SensorInfoViewHolder extends RecyclerView.ViewHolder { // Text fields in the item view TextView name, vendor, resolution, power, version; - /** - * {@inheritDoc} - * Assign TextView fields corresponding to SensorInfo attributes. - * - * @see com.openpositioning.PositionMe.sensors.SensorInfo the data class - */ + // {@inheritDoc} + // Assign TextView fields corresponding to SensorInfo attributes. +// Related: com.openpositioning.PositionMe.sensors.SensorInfo the data class public SensorInfoViewHolder(@NonNull View itemView) { super(itemView); name = itemView.findViewById(R.id.sensorNameItem); @@ -36,3 +29,5 @@ public SensorInfoViewHolder(@NonNull View itemView) { version = itemView.findViewById(R.id.sensorVersionItem); } } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/TrajDownloadListAdapter.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/TrajDownloadListAdapter.java index 7de29c8a..0d5d261c 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/TrajDownloadListAdapter.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/TrajDownloadListAdapter.java @@ -35,16 +35,14 @@ import java.util.List; import java.util.Map; -/** - * Adapter used for displaying trajectory metadata in a RecyclerView list. - * This adapter binds trajectory metadata from the server to individual view items. - * The download status is indicated via a button with different icons. - * The adapter also listens for file changes using FileObserver to update the download records in real time. - * A local set of "downloading" trajectory IDs is maintained to support simultaneous downloads. - * @see TrajDownloadViewHolder for the corresponding view holder. - * @see FilesFragment for details on how the data is generated. - * @see ServerCommunications for where the response items are received. - */ +// Adapter used for displaying trajectory metadata in a RecyclerView list. +// This adapter binds trajectory metadata from the server to individual view items. +// The download status is indicated via a button with different icons. +// The adapter also listens for file changes using FileObserver to update the download records in real time. +// A local set of "downloading" trajectory IDs is maintained to support simultaneous downloads. +// Related: TrajDownloadViewHolder for the corresponding view holder. +// Related: FilesFragment for details on how the data is generated. +// Related: ServerCommunications for where the response items are received. public class TrajDownloadListAdapter extends RecyclerView.Adapter { // Date-time formatter used to format date and time. @@ -60,13 +58,10 @@ public class TrajDownloadListAdapter extends RecyclerView.Adapter downloadingTrajIds = new HashSet<>(); - /** - * Constructor for the adapter. - * - * @param context Application context used for inflating layouts. - * @param responseItems List of response items from the server. - * @param listener Callback listener for handling download click events. - */ + // Constructor for the adapter. +// Parameter context: Application context used for inflating layouts. +// Parameter responseItems: List of response items from the server. +// Parameter listener: Callback listener for handling download click events. public TrajDownloadListAdapter(Context context, List> responseItems, DownloadClickListener listener) { this.context = context; this.responseItems = responseItems; @@ -77,12 +72,10 @@ public TrajDownloadListAdapter(Context context, List> respon initFileObserver(); } - /** - * Loads the local download records from storage. - * The records are stored in a JSON file located in the app-specific Downloads directory. - * After loading, any trajectory IDs that have now finished downloading are removed - * from the downloading set. - */ + // Loads the local download records from storage. + // The records are stored in a JSON file located in the app-specific Downloads directory. + // After loading, any trajectory IDs that have now finished downloading are removed + // from the downloading set. private void loadDownloadRecords() { try { File file = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "download_records.json"); @@ -132,10 +125,8 @@ private void loadDownloadRecords() { } } - /** - * Initializes the FileObserver to listen for modifications on the "download_records.json" file. - * When the file is modified, it reloads the download records and refreshes the UI. - */ + // Initializes the FileObserver to listen for modifications on the "download_records.json" file. + // When the file is modified, it reloads the download records and refreshes the UI. private void initFileObserver() { File downloadsFolder = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS); if (downloadsFolder == null) { @@ -158,13 +149,10 @@ public void onEvent(int event, String path) { fileObserver.startWatching(); } - /** - * Creates a new view holder for a trajectory item. - * - * @param parent The parent view group. - * @param viewType The view type. - * @return A new instance of TrajDownloadViewHolder. - */ + // Creates a new view holder for a trajectory item. +// Parameter parent: The parent view group. +// Parameter viewType: The view type. +// Returns: A new instance of TrajDownloadViewHolder. @NonNull @Override public TrajDownloadViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @@ -172,16 +160,14 @@ public TrajDownloadViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int .inflate(R.layout.item_trajectorycard_view, parent, false), listener); } - /** - * Binds data to the view holder. - * Formats and assigns trajectory metadata fields to the corresponding views. - * The button state is determined as follows: - * - If the trajectory is present in the download records, it is set as "downloaded". - * - Else if the trajectory is in the downloading set, it is set as "downloading". - * - Otherwise, it is set as "not downloaded". - * @param holder The view holder to bind data to. - * @param position The position of the item in the list. - */ + // Binds data to the view holder. + // Formats and assigns trajectory metadata fields to the corresponding views. + // The button state is determined as follows: + // If the trajectory is present in the download records, it is set as "downloaded". + // Else if the trajectory is in the downloading set, it is set as "downloading". + // Otherwise, it is set as "not downloaded". +// Parameter holder: The view holder to bind data to. +// Parameter position: The position of the item in the list. @Override public void onBindViewHolder(@NonNull TrajDownloadViewHolder holder, int position) { // Retrieve the trajectory id from the response item. @@ -258,25 +244,19 @@ public void onBindViewHolder(@NonNull TrajDownloadViewHolder holder, int positio holder.downloadButton.invalidate(); } - /** - * Returns the number of items in the response list. - * - * @return The size of the responseItems list. - */ + // Returns the number of items in the response list. +// Returns: The size of the responseItems list. @Override public int getItemCount() { return responseItems.size(); } - /** - * Sets the appearance of the button based on its state. - * - * @param button The MaterialButton to update. - * @param state The state of the button: - * 0 - Not downloaded, - * 1 - Downloaded, - * 2 - Downloading. - */ + // Sets the appearance of the button based on its state. +// Parameter button: The MaterialButton to update. +// Parameter state: The state of the button: + // 0 - Not downloaded, + // 1 - Downloaded, + // 2 - Downloading. private void setButtonState(MaterialButton button, int state) { if (state == 1) { button.setIconResource(R.drawable.ic_baseline_play_circle_filled_24); @@ -293,3 +273,5 @@ private void setButtonState(MaterialButton button, int state) { } } } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/TrajDownloadViewHolder.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/TrajDownloadViewHolder.java index af14249f..66afce10 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/TrajDownloadViewHolder.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/TrajDownloadViewHolder.java @@ -12,14 +12,10 @@ import java.lang.ref.WeakReference; -/** - * View holder class for the RecyclerView displaying Trajectory download data. - * - * @see TrajDownloadListAdapter the corresponding list adapter. - * @see com.openpositioning.PositionMe.R.layout#item_trajectorycard_view xml layout file - * - * @author Mate Stodulka - */ +// View holder class for the RecyclerView displaying Trajectory download data. +// Related: TrajDownloadListAdapter the corresponding list adapter. +// Related: com.openpositioning.PositionMe.R.layout#item_trajectorycard_view xml layout file +// @author Mate Stodulka public class TrajDownloadViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { private final TextView trajId; @@ -27,13 +23,10 @@ public class TrajDownloadViewHolder extends RecyclerView.ViewHolder implements V final MaterialButton downloadButton; private final WeakReference listenerReference; - /** - * {@inheritDoc} - * Assign TextView fields corresponding to Trajectory metadata. - * - * @param listener DownloadClickListener to enable acting on clicks on items. - * @see FilesFragment generating the data and implementing the listener. - */ + // {@inheritDoc} + // Assign TextView fields corresponding to Trajectory metadata. +// Parameter listener: DownloadClickListener to enable acting on clicks on items. +// Related: FilesFragment generating the data and implementing the listener. public TrajDownloadViewHolder(@NonNull View itemView, DownloadClickListener listener) { super(itemView); this.listenerReference = new WeakReference<>(listener); @@ -44,23 +37,17 @@ public TrajDownloadViewHolder(@NonNull View itemView, DownloadClickListener list this.downloadButton.setOnClickListener(this); } - /** - * Public getter for trajId. - */ + // Public getter for trajId. public TextView getTrajId() { return trajId; } - /** - * Public getter for trajDate. - */ + // Public getter for trajDate. public TextView getTrajDate() { return trajDate; } - /** - * Calls the onPositionClick function on the listenerReference object. - */ + // Calls the onPositionClick function on the listenerReference object. @Override public void onClick(View view) { listenerReference.get().onPositionClicked(getAdapterPosition()); @@ -73,3 +60,5 @@ public void onClick(View view) { } } } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/UploadListAdapter.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/UploadListAdapter.java index b564e231..600ac089 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/UploadListAdapter.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/UploadListAdapter.java @@ -13,96 +13,95 @@ import java.io.File; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Adapter used for displaying local Trajectory file data - * - * @see UploadViewHolder corresponding View Holder class - * @see com.openpositioning.PositionMe.R.layout#item_upload_card_view xml layout file - * - * @author Mate Stodulka - */ + +// Adapter used for displaying local Trajectory file data +// FINAL VERSION: Correctly parses names by finding the last underscore. public class UploadListAdapter extends RecyclerView.Adapter { private final Context context; private final List uploadItems; private final DownloadClickListener listener; - /** - * Default public constructor with context for inflating views and list to be displayed. - * - * @param context application context to enable inflating views used in the list. - * @param uploadItems List of trajectory Files found locally on the device. - * @param listener clickListener to download trajectories when clicked. - * - * @see com.openpositioning.PositionMe.Traj protobuf objects exchanged with the server. - */ public UploadListAdapter(Context context, List uploadItems, DownloadClickListener listener) { this.context = context; this.uploadItems = uploadItems; this.listener = listener; } - /** - * {@inheritDoc} - * - * @see com.openpositioning.PositionMe.R.layout#item_upload_card_view xml layout file - */ @NonNull @Override public UploadViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new UploadViewHolder(LayoutInflater.from(context).inflate(R.layout.item_upload_card_view, parent, false), listener); } - /** - * {@inheritDoc} - * Formats and assigns the data fields from the local Trajectory Files object to the TextView fields. - * - * @see UploadFragment finding the data from on local storage. - * @see com.openpositioning.PositionMe.R.layout#item_upload_card_view xml layout file. - */ + // Parse filename format: traj_NAME_DATE.txt + // Extract name (remove "traj_" prefix) and date. @Override public void onBindViewHolder(@NonNull UploadViewHolder holder, int position) { - holder.trajId.setText(String.valueOf(position)); - Pattern datePattern = Pattern.compile("_(.*?)\\.txt"); - Matcher dateMatcher = datePattern.matcher(uploadItems.get(position).getName()); - String dateString = dateMatcher.find() ? dateMatcher.group(1) : "N/A"; - System.err.println("UPLOAD - Date string: " + dateString); - holder.trajDate.setText(dateString); - - // Set click listener for the delete button - holder.deletebutton.setOnClickListener(v -> deleteFileAtPosition(position)); + // Get filename (e.g. "traj_MyWalk_2026-02-05.txt") + File currentFile = uploadItems.get(position); + String fileName = currentFile.getName(); + + // Remove ".txt" suffix + if (fileName.endsWith(".txt")) { + fileName = fileName.substring(0, fileName.length() - 4); + } + + // Find last "_" to split name and date + int lastUnderscoreIndex = fileName.lastIndexOf("_"); + + if (lastUnderscoreIndex != -1) { + // Extract date + String datePart = fileName.substring(lastUnderscoreIndex + 1); + holder.trajDate.setText(datePart); + + // Extract name + String namePart = fileName.substring(0, lastUnderscoreIndex); + + // Remove "traj_" prefix if exists + if (namePart.startsWith("traj_")) { + namePart = namePart.substring(5); + } + + // Display name or "Unnamed" if empty + if (namePart.isEmpty()) { + holder.trajId.setText("Unnamed"); + } else { + // Display actual name (e.g. "MyWalk") + // For legacy files like "traj_trajectory_DATE" + holder.trajId.setText(namePart); + } + holder.trajId.setTextSize(20); + + } else { + // Edge case: filename without underscore + holder.trajDate.setText("N/A"); + holder.trajId.setText(fileName); + holder.trajId.setTextSize(14); + } + // Set up delete button + holder.deletebutton.setOnClickListener(v -> deleteFileAtPosition(position)); } - /** - * {@inheritDoc} - * Number of local files. - */ @Override public int getItemCount() { return uploadItems.size(); } - private void deleteFileAtPosition(int position) - { - if (position >= 0 && position < uploadItems.size()) - { + private void deleteFileAtPosition(int position) { + if (position >= 0 && position < uploadItems.size()) { File fileToDelete = uploadItems.get(position); - if (fileToDelete.exists() && fileToDelete.delete()) - { + if (fileToDelete.exists() && fileToDelete.delete()) { uploadItems.remove(position); notifyItemRemoved(position); - notifyItemRangeChanged(position, uploadItems.size()); // Update subsequent items + notifyItemRangeChanged(position, uploadItems.size()); Toast.makeText(context, "File deleted successfully", Toast.LENGTH_SHORT).show(); - } - else - { + } else { Toast.makeText(context, "Failed to delete file", Toast.LENGTH_SHORT).show(); } } } } + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/UploadViewHolder.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/UploadViewHolder.java index e6068969..6ea7b3ee 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/UploadViewHolder.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/UploadViewHolder.java @@ -13,14 +13,10 @@ import java.lang.ref.WeakReference; -/** - * View holder class for the RecyclerView displaying Trajectory files to be uploaded. - * - * @see UploadListAdapter the corresponding list adapter. - * @see com.openpositioning.PositionMe.R.layout#item_upload_card_view xml layout file - * - * @author Mate Stodulka - */ +// View holder class for the RecyclerView displaying Trajectory files to be uploaded. +// Related: UploadListAdapter the corresponding list adapter. +// Related: com.openpositioning.PositionMe.R.layout#item_upload_card_view xml layout file +// @author Mate Stodulka public class UploadViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { TextView trajId; @@ -30,15 +26,11 @@ public class UploadViewHolder extends RecyclerView.ViewHolder implements View.On private WeakReference listenerReference; public Button deletebutton; - /** - * {@inheritDoc} - * Assign TextView fields corresponding to Trajectory file metadata. - * - * @param listener DownloadClickListener to enable acting on clicks on items. - * - * @see UploadFragment locating the data and implementing the - * listener. - */ + // {@inheritDoc} + // Assign TextView fields corresponding to Trajectory file metadata. +// Parameter listener: DownloadClickListener to enable acting on clicks on items. +// Related: UploadFragment locating the data and implementing the + // listener. public UploadViewHolder(@NonNull View itemView, DownloadClickListener listener) { super(itemView); @@ -51,12 +43,12 @@ public UploadViewHolder(@NonNull View itemView, DownloadClickListener listener) this.deletebutton = itemView.findViewById(R.id.deletebutton); } - /** - * {@inheritDoc} - * Calls the onPositionClick function on the listenerReference object. - */ + // {@inheritDoc} + // Calls the onPositionClick function on the listenerReference object. @Override public void onClick(View view) { listenerReference.get().onPositionClicked(getAdapterPosition()); } } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/WifiListAdapter.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/WifiListAdapter.java index 887e7689..6b6f586b 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/WifiListAdapter.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/WifiListAdapter.java @@ -12,66 +12,60 @@ import java.util.List; -/** - * Adapter used for displaying wifi network data. - * - * @see WifiViewHolder corresponding View Holder class - * @see com.openpositioning.PositionMe.R.layout#item_wifi_card_view xml layout file - * - * @author Mate Stodulka - */ +// Adapter used for displaying wifi network data. +// Related: WifiViewHolder corresponding View Holder class +// Related: com.openpositioning.PositionMe.R.layout#item_wifi_card_view xml layout file +// @author Mate Stodulka public class WifiListAdapter extends RecyclerView.Adapter { Context context; List items; - /** - * Default public constructor with context for inflating views and list to be displayed. - * - * @param context application context to enable inflating views used in the list. - * @param items list of Wifi objects to be displayed in the list. - * - * @see Wifi the data class. - */ + // Default public constructor with context for inflating views and list to be displayed. +// Parameter context: application context to enable inflating views used in the list. +// Parameter items: list of Wifi objects to be displayed in the list. +// Related: Wifi the data class. public WifiListAdapter(Context context, List items) { this.context = context; this.items = items; } - /** - * {@inheritDoc} - * @see com.openpositioning.PositionMe.R.layout#item_wifi_card_view xml layout file - */ + // {@inheritDoc} +// Related: com.openpositioning.PositionMe.R.layout#item_wifi_card_view xml layout file @NonNull @Override public WifiViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new WifiViewHolder(LayoutInflater.from(context).inflate(R.layout.item_wifi_card_view,parent,false)); } - /** - * {@inheritDoc} - * Formats and assigns the data fields from the Wifi object to the TextView fields. - * - * @see Wifi data class - * @see com.openpositioning.PositionMe.R.string formatting for strings. - * @see com.openpositioning.PositionMe.R.layout#item_wifi_card_view xml layout file - */ + // {@inheritDoc} + // Formats and assigns the data fields from the Wifi object to the TextView fields. +// Related: Wifi data class +// Related: com.openpositioning.PositionMe.R.string formatting for strings. +// Related: com.openpositioning.PositionMe.R.layout#item_wifi_card_view xml layout file @Override public void onBindViewHolder(@NonNull WifiViewHolder holder, int position) { - String macString = context.getString(R.string.mac, Long.toString(items.get(position).getBssid())); - holder.bssid.setText(macString); - String levelString = context.getString(R.string.db, Long.toString(items.get(position).getLevel())); - holder.level.setText(levelString); + Wifi wifi = items.get(position); + + // Check if this is a placeholder item (BSSID = 0) + if (wifi.getBssid() == 0) { + holder.bssid.setText(wifi.getSsid()); // Display "Scanning..." or similar message + holder.level.setText(""); + } else { + String macString = context.getString(R.string.mac, Long.toString(wifi.getBssid())); + holder.bssid.setText(macString); + String levelString = context.getString(R.string.db, Long.toString(wifi.getLevel())); + holder.level.setText(levelString); + } } - /** - * {@inheritDoc} - * Number of Wifi objects. - * - * @see Wifi - */ + // {@inheritDoc} + // Number of Wifi objects. +// Related: Wifi @Override public int getItemCount() { return items.size(); } } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/WifiViewHolder.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/WifiViewHolder.java index 96c563cf..e40cfc7f 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/WifiViewHolder.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/viewitems/WifiViewHolder.java @@ -8,28 +8,23 @@ import com.openpositioning.PositionMe.R; -/** - * View holder class for the RecyclerView displaying Wifi data. - * - * @see WifiListAdapter the corresponding list adapter. - * @see com.openpositioning.PositionMe.R.layout#item_wifi_card_view xml layout file - * - * @author Mate Stodulka - */ +// View holder class for the RecyclerView displaying Wifi data. +// Related: WifiListAdapter the corresponding list adapter. +// Related: com.openpositioning.PositionMe.R.layout#item_wifi_card_view xml layout file +// @author Mate Stodulka public class WifiViewHolder extends RecyclerView.ViewHolder { TextView bssid; TextView level; - /** - * {@inheritDoc} - * Assign TextView fields corresponding to Wifi attributes. - * - * @see com.openpositioning.PositionMe.sensors.Wifi the data class - */ + // {@inheritDoc} + // Assign TextView fields corresponding to Wifi attributes. +// Related: com.openpositioning.PositionMe.sensors.Wifi the data class public WifiViewHolder(@NonNull View itemView) { super(itemView); bssid = itemView.findViewById(R.id.wifiNameItem); level = itemView.findViewById(R.id.wifiLevelItem); } } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/BleDevice.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/BleDevice.java new file mode 100644 index 00000000..7616214a --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/BleDevice.java @@ -0,0 +1,32 @@ +package com.openpositioning.PositionMe.sensors; + +// Data class for Bluetooth Low Energy (BLE) devices. +// Holds MAC address, device name, and signal strength (RSSI) for discovered BLE devices. +public class BleDevice { + private String macAddress; + private String name; + private int rssi; + + public BleDevice() {} + + public BleDevice(String macAddress, String name, int rssi) { + this.macAddress = macAddress; + this.name = name; + this.rssi = rssi; + } + + public String getMacAddress() { return macAddress; } + public String getName() { return name; } + public int getRssi() { return rssi; } + + public void setMacAddress(String macAddress) { this.macAddress = macAddress; } + public void setName(String name) { this.name = name; } + public void setRssi(int rssi) { this.rssi = rssi; } + + @Override + public String toString() { + return "MAC: " + macAddress + ", RSSI: " + rssi + " dBm"; + } +} + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/GNSSDataProcessor.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/GNSSDataProcessor.java index 579e344c..836ebf21 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/sensors/GNSSDataProcessor.java +++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/GNSSDataProcessor.java @@ -4,77 +4,82 @@ import android.annotation.SuppressLint; import android.content.Context; import android.content.pm.PackageManager; +import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; +import android.os.Looper; +import android.util.Log; import android.widget.Toast; import androidx.core.app.ActivityCompat; -/** - * Class for handling and recording location data. - * - * The class is responsibly for handling location data from GNSS and cellular sources using the - * Android LocationManager class. - * - * @author Virginia Cangelosi - * @author Mate Stodulka - */ +import com.google.android.gms.location.FusedLocationProviderClient; +import com.google.android.gms.location.LocationCallback; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.LocationResult; +import com.google.android.gms.location.LocationServices; +import com.google.android.gms.location.Priority; + +// Class for handling and recording location data. +// Uses Google's FusedLocationProviderClient for maximum accuracy by combining +// GPS, WiFi, cell towers, and device sensors automatically. +// Falls back to raw LocationManager if FusedLocation is unavailable. +// @author Virginia Cangelosi +// @author Mate Stodulka public class GNSSDataProcessor { - // Application context for handling permissions and locationManager instances + private static final String TAG = "GNSSDataProcessor"; + private final Context context; - // Locations manager to enable access to GNSS and cellular location data via the android system private LocationManager locationManager; - // Location listener to receive the location data broadcast by the system private LocationListener locationListener; + // Google Fused Location (high accuracy) + private FusedLocationProviderClient fusedLocationClient; + private LocationCallback fusedLocationCallback; + private boolean usingFusedLocation = false; - /** - * Public default constructor of the GNSSDataProcessor class. - * - * The constructor saves the context, checks for permissions to use the location services, - * creates an instance of the shared preferences to access settings using the context, - * initialises the location manager, and the location listener that will receive the data in the - * class the called the constructor. It checks if GPS and cellular networks are available and - * notifies the user via toasts if they need to be turned on. If permissions are granted it - * starts the location information gathering process. - * - * @param context Application Context to be used for permissions and device accesses. - * @param locationListener Location listener that will receive the location information from - * the device broadcasts. - * - * @see SensorFusion the intended parent class. - */ + // Public default constructor of the GNSSDataProcessor class. + // Uses Google FusedLocationProviderClient as primary source for maximum accuracy. + // Falls back to raw GPS + Network providers if fused location is unavailable. public GNSSDataProcessor(Context context, LocationListener locationListener) { this.context = context; + this.locationListener = locationListener; - // Check for permissions boolean permissionsGranted = checkLocationPermissions(); - //Location manager and listener + // Initialize LocationManager as fallback this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); - this.locationListener = locationListener; - // Turn on gps if it is currently disabled + // Initialize Google Fused Location Client (primary - highest accuracy) + try { + this.fusedLocationClient = LocationServices.getFusedLocationProviderClient(context); + this.fusedLocationCallback = new LocationCallback() { + @Override + public void onLocationResult(LocationResult locationResult) { + if (locationResult == null) return; + // Use the most recent location + Location location = locationResult.getLastLocation(); + if (location != null) { + // Forward to existing LocationListener for compatibility with SensorFusion + locationListener.onLocationChanged(location); + } + } + }; + Log.d(TAG, "FusedLocationProviderClient initialized"); + } catch (Exception e) { + Log.w(TAG, "FusedLocationProvider unavailable, will use raw GPS: " + e.getMessage()); + this.fusedLocationClient = null; + } + if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { Toast.makeText(context, "Open GPS", Toast.LENGTH_SHORT).show(); } - if (!locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { - Toast.makeText(context, "Enable Cellular", Toast.LENGTH_SHORT).show(); - } - // Start location updates + if (permissionsGranted) { startLocationUpdates(); } } - /** - * Checks if the user authorised all permissions necessary for accessing location data. - * - * Explicit user permissions must be granted for android sdk version 23 and above. This - * function checks which permissions are granted, and returns their conjunction. - * - * @return boolean true if all permissions are granted for location access, false otherwise. - */ private boolean checkLocationPermissions() { int coarseLocationPermission = ActivityCompat.checkSelfPermission(this.context, Manifest.permission.ACCESS_COARSE_LOCATION); @@ -83,42 +88,66 @@ private boolean checkLocationPermissions() { int internetPermission = ActivityCompat.checkSelfPermission(this.context, Manifest.permission.INTERNET); - // Return missing permissions return coarseLocationPermission == PackageManager.PERMISSION_GRANTED && fineLocationPermission == PackageManager.PERMISSION_GRANTED && internetPermission == PackageManager.PERMISSION_GRANTED; } - /** - * Request location updates via the GNSS and Cellular networks. - * - * The function checks for permissions again, and then requests updates via the location - * manager to the location listener. If permissions are granted but the GPS and cellular - * networks are disabled it reminds the user via toasts to turn them on. - */ + // Start location updates using Google Fused Location (primary) + raw GPS (fallback). + // FusedLocation combines GPS, WiFi, cell, and sensors for best accuracy. @SuppressLint("MissingPermission") public void startLocationUpdates() { - //if (sharedPreferences.getBoolean("location", true)) { boolean permissionGranted = checkLocationPermissions(); - if (permissionGranted && locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) && - locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)){ + if (!permissionGranted) return; - locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, locationListener); - locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, locationListener); + // PRIMARY: Google Fused Location Provider - highest accuracy + if (fusedLocationClient != null) { + try { + LocationRequest locationRequest = new LocationRequest.Builder( + Priority.PRIORITY_HIGH_ACCURACY, 300) // 300ms interval (balanced for smoothing) + .setMinUpdateIntervalMillis(200) // fastest 200ms (avoid too frequent updates) + .setMinUpdateDistanceMeters(0.5f) // small distance threshold + .setWaitForAccurateLocation(false) // don't wait, send immediately + .build(); + + fusedLocationClient.requestLocationUpdates(locationRequest, + fusedLocationCallback, Looper.getMainLooper()); + usingFusedLocation = true; + Log.d(TAG, "Started FusedLocation updates (HIGH_ACCURACY, 300ms, smooth mode)"); + } catch (Exception e) { + Log.w(TAG, "FusedLocation failed, falling back to raw GPS: " + e.getMessage()); + usingFusedLocation = false; + } } - else if(permissionGranted && !locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)){ + + // FALLBACK/SUPPLEMENT: Raw GPS provider (always start for redundancy) + if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, 500, 0, locationListener); + Log.d(TAG, "Started raw GPS updates"); + } else { Toast.makeText(context, "Open GPS", Toast.LENGTH_LONG).show(); } - else if(permissionGranted && !locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)){ - Toast.makeText(context, "Turn on WiFi", Toast.LENGTH_LONG).show(); + + // SUPPLEMENT: Network provider for faster initial fix + if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { + locationManager.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, 1000, 0, locationListener); + Log.d(TAG, "Started Network location updates"); } } - /** - * Stops updates to the location listener via the location manager. - */ + // Stops all location updates. public void stopUpdating() { + // Stop fused location + if (fusedLocationClient != null && fusedLocationCallback != null) { + fusedLocationClient.removeLocationUpdates(fusedLocationCallback); + Log.d(TAG, "Stopped FusedLocation updates"); + } + // Stop raw GPS/Network locationManager.removeUpdates(locationListener); + Log.d(TAG, "Stopped raw GPS/Network updates"); } - } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/MovementSensor.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/MovementSensor.java index a5282150..e2b2ec0b 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/sensors/MovementSensor.java +++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/MovementSensor.java @@ -4,17 +4,13 @@ import android.hardware.Sensor; import android.hardware.SensorManager; -/** - * Movement sensor class representing all Sensor Manager based devices. - * - * The class is initialised with the application context to be used for permissions and hardware - * access. Using the context, it adds a Sensor Manager, as well as a Sensor and {@link SensorInfo} - * instance, with the type of the sensor determined upon initialisation of the class. - * - * @see SensorFusion where instances of this class are intended to be used. - * - * @author Mate Stodulka - */ +// Movement sensor class representing all Sensor Manager based devices. +// The class is initialised with the application context to be used for permissions and hardware +// access. Using the context, it adds a Sensor Manager, as well as a Sensor and +// {@link com.openpositioning.PositionMe.sensors.SensorInfo} +// instance, with the type of the sensor determined upon initialisation of the class. +// Related: com.openpositioning.PositionMe.sensors.SensorFusion where instances of this class are intended to be used. +// @author Mate Stodulka public class MovementSensor { // Application context for permissions and hardware access protected Context context; @@ -26,16 +22,11 @@ public class MovementSensor { protected SensorInfo sensorInfo; - /** - * Public default constructor for the Movement Sensor class. - * - * It calls the superclass constructor with context, and then initialises local properties. - * - * @param context Application context used to check permissions and access devices. - * @param sensorType Type of the sensor to be created, using Sensor.TYPE constants. - * - * @see SensorInfo objects holding physical sensors properties. - */ + // Public default constructor for the Movement Sensor class. + // It calls the superclass constructor with context, and then initialises local properties. +// Parameter context: Application context used to check permissions and access devices. +// Parameter sensorType: Type of the sensor to be created, using Sensor.TYPE constants. +// Related: com.openpositioning.PositionMe.sensors.SensorInfo objects holding physical sensors properties. public MovementSensor(Context context, int sensorType) { this.context = context; @@ -66,3 +57,5 @@ public MovementSensor(Context context, int sensorType) { } } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/Observable.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/Observable.java index dc7e0c73..fec5c06c 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/sensors/Observable.java +++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/Observable.java @@ -1,27 +1,19 @@ package com.openpositioning.PositionMe.sensors; -/** - * Interface for observable class. - * - * Simplified version of default Observable interface, with only the resgisterObserver and - * notifyObservers methods included see {@link java.util.Observable}. - * - * @author Virginia Cangelosi - * @author Mate Stodulka - */ +// Interface for observable class. +// Simplified version of default Observable interface, with only the resgisterObserver and +// notifyObservers methods included see {@link java.util.Observable}. +// @author Virginia Cangelosi +// @author Mate Stodulka public interface Observable { - /** - * Register an object implementing the {@link Observer} interface to listen for updates. - * - * @param o instance of a class implementing the Observer interface - */ + // Register an object implementing the {@link Observer} interface to listen for updates. +// Parameter o: instance of a class implementing the Observer interface public void registerObserver(com.openpositioning.PositionMe.sensors.Observer o); - /** - * Notify observers of changes to relevant data structures. If there are multiple data structures - * and not all are relevant to all observers, use the input to differentiate. - * - * @param idx int index signaling which data-structure was updated. - */ + // Notify observers of changes to relevant data structures. If there are multiple data structures + // and not all are relevant to all observers, use the input to differentiate. +// Parameter idx: int index signaling which data-structure was updated. public void notifyObservers(int idx); } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/Observer.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/Observer.java index 765b1a0b..95438203 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/sensors/Observer.java +++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/Observer.java @@ -1,21 +1,16 @@ package com.openpositioning.PositionMe.sensors; -/** - * Interface for observers of an observable class. - * - * Simplified version of default Observer interface, with only the update method included - * see {@link java.util.Observer}. - * - * @author Virginia Cangelosi - * @author Mate Stodulka - */ +// Interface for observers of an observable class. +// Simplified version of default Observer interface, with only the update method included +// see {@link java.util.Observer}. +// @author Virginia Cangelosi +// @author Mate Stodulka public interface Observer { - /** - * Updates from the class implementing the {@link Observable} interface, where this instance is - * registered as an observer. - * - * @param objList an array of objects that were updated in the Observable - */ + // Updates from the class implementing the {@link Observable} interface, where this instance is + // registered as an observer. +// Parameter objList: an array of objects that were updated in the Observable public void update(Object[] objList); } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorFusion.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorFusion.java index 6eca847c..716ab029 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorFusion.java +++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorFusion.java @@ -1,7 +1,15 @@ package com.openpositioning.PositionMe.sensors; +import android.Manifest; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothManager; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanResult; import android.content.Context; +import com.openpositioning.PositionMe.BuildConfig; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; @@ -12,17 +20,21 @@ import android.os.PowerManager; import android.os.SystemClock; import android.util.Log; +import android.view.Surface; +import android.view.WindowManager; +import com.openpositioning.PositionMe.Traj; import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; import androidx.preference.PreferenceManager; import com.google.android.gms.maps.model.LatLng; -import com.openpositioning.PositionMe.presentation.activity.MainActivity; +import com.openpositioning.PositionMe.utils.BuildingPolygon; import com.openpositioning.PositionMe.utils.PathView; import com.openpositioning.PositionMe.utils.PdrProcessing; +import com.openpositioning.PositionMe.utils.CircularFloatBuffer; +import com.openpositioning.PositionMe.utils.GeometryUtils; import com.openpositioning.PositionMe.data.remote.ServerCommunications; -import com.openpositioning.PositionMe.Traj; -import com.openpositioning.PositionMe.presentation.fragment.SettingsFragment; import org.json.JSONException; import org.json.JSONObject; @@ -36,57 +48,66 @@ import java.util.stream.Collectors; import java.util.stream.Stream; - -/** - * The SensorFusion class is the main data gathering and processing class of the application. - * - * It follows the singleton design pattern to ensure that every fragment and process has access to - * the same date and sensor instances. Hence it has a private constructor, and must be initialised - * with the application context after creation. - *

- * The class implements {@link SensorEventListener} and has instances of {@link MovementSensor} for - * every device type necessary for data collection. As such, it implements the - * {@link SensorFusion#onSensorChanged(SensorEvent)} function, and process and records the data - * provided by the sensor hardware, which are stored in a {@link Traj} object. Data is read - * continuously but is only saved to the trajectory when recording is enabled. - *

- * The class provides a number of setters and getters so that other classes can have access to the - * sensor data and influence the behaviour of data collection. - * - * @author Michal Dvorak - * @author Mate Stodulka - * @author Virginia Cangelosi - */ +// Central sensor fusion pipeline for recording and live positioning. +// Responsibilities: +// Collect IMU, GNSS, WiFi and BLE streams. +// Maintain PDR state and fused position updates. +// Build trajectory protobuf payloads during recording. +// Expose live values for UI and replay modules. public class SensorFusion implements SensorEventListener, Observer { - // Store the last event timestamps for each sensor type private HashMap lastEventTimestamps = new HashMap<>(); private HashMap eventCounts = new HashMap<>(); - long maxReportLatencyNs = 0; // Disable batching to deliver events immediately - - // Define a threshold for large time gaps (in milliseconds) - private static final long LARGE_GAP_THRESHOLD_MS = 500; // Adjust this if needed + long maxReportLatencyNs = 0; + private static final long LARGE_GAP_THRESHOLD_MS = 500; - //region Static variables - // Singleton Class + // region Static variables private static final SensorFusion sensorFusion = new SensorFusion(); - // Static constant for calculations with milliseconds + // IMU data sampling period for periodic trajectory snapshots (10ms = 100Hz). private static final long TIME_CONST = 10; - // Coefficient for fusing gyro-based and magnetometer-based orientation - public static final float FILTER_COEFFICIENT = 0.96f; - //Tuning value for low pass filter + public static final float FILTER_COEFFICIENT = 0.94f; private static final float ALPHA = 0.8f; - // String for creating WiFi fingerprint JSO N object private static final String WIFI_FINGERPRINT= "wf"; - //endregion - - //region Instance variables - // Keep device awake while recording + private static final int BAROMETER_ALTITUDE_WINDOW = 2; + private static final int GNSS_ALTITUDE_WINDOW = 1; + private static final int MOTION_WINDOW = 12; + private static final float STATIONARY_ACCEL_THRESHOLD = 0.18f; + private static final float STATIONARY_SPEED_THRESHOLD = 0.6f; + private static final float TRUSTED_VERTICAL_ACCURACY_METERS = 8f; + private static final float ACCEPTABLE_VERTICAL_ACCURACY_METERS = 12f; + private static final float ACCEPTABLE_HORIZONTAL_ACCURACY_METERS = 12f; + private static final float MAX_ALTITUDE_OFFSET_JUMP_METERS = 18f; + private static final float MIN_SEA_LEVEL_PRESSURE_HPA = 870f; + private static final float MAX_SEA_LEVEL_PRESSURE_HPA = 1085f; + private static final long STATIONARY_STEP_GAP_MS = 1000L; + // Gains used while calibrating barometer altitude against GNSS altitude. + private static final float SEA_LEVEL_GAIN_STATIONARY = 0.42f; + private static final float SEA_LEVEL_GAIN_MOVING = 0.24f; + private static final float OFFSET_CALIBRATION_GAIN_STATIONARY = 0.58f; + private static final float OFFSET_CALIBRATION_GAIN_MOVING = 0.36f; + private static final float ALTITUDE_SMOOTHING_GAIN_STATIONARY = 0.78f; + private static final float ALTITUDE_SMOOTHING_GAIN_MOVING = 0.58f; + private static final long ALTITUDE_FAST_CONVERGENCE_WINDOW_MS = 3000L; + private static final float SEA_LEVEL_GAIN_FAST = 0.95f; + private static final float OFFSET_CALIBRATION_GAIN_FAST = 0.97f; + private static final float ALTITUDE_SMOOTHING_GAIN_FAST = 0.97f; + private static final double WIFI_STABLE_DISTANCE_METERS = 3.0; + private static final int WIFI_STABLE_CONFIRMATIONS_MOVING = 0; + private static final int WIFI_STABLE_CONFIRMATIONS_STATIONARY = 3; + private static final double WIFI_STATIONARY_FUSED_GATE_METERS = 4.0; + private static final double WIFI_STATIONARY_ACCEPTED_GATE_METERS = 3.0; + // Absolute altitude bands used by barometer-based floor estimation. + private static final float FLOOR_BAND_B1_MAX = 128.5f; + private static final float FLOOR_BAND_GF_MAX = 132.75f; + private static final float FLOOR_BAND_F1_MAX = 137.5f; + private static final float FLOOR_BAND_F2_MAX = 142.7f; + private static final float FLOOR_BAND_HYSTERESIS_METERS = 0.6f; + // endregion + + // region Instance variables private PowerManager.WakeLock wakeLock; private Context appContext; - - // Settings private SharedPreferences settings; // Movement sensor instances @@ -100,127 +121,155 @@ public class SensorFusion implements SensorEventListener, Observer { private MovementSensor rotationSensor; private MovementSensor gravitySensor; private MovementSensor linearAccelerationSensor; - // Other data recording + + // Bluetooth components + private BluetoothAdapter bluetoothAdapter; + private BluetoothLeScanner bluetoothLeScanner; + private ScanCallback bleScanCallback; + private WifiDataProcessor wifiProcessor; private GNSSDataProcessor gnssProcessor; - // Data listener private final LocationListener locationListener; - // Server communication class for sending data private ServerCommunications serverCommunications; - // Trajectory object containing all data + + // Protobuf Builder private Traj.Trajectory.Builder trajectory; - // Settings private boolean saveRecording; private float filter_coefficient; - // Variables to help with timed events + private long absoluteStartTime; private long bootTime; long lastStepTime = 0; - // Timer object for scheduling data recording private Timer storeTrajectoryTimer; - // Counters for dividing timer to record data every 1 second/ every 5 seconds private int counter; private int secondCounter; + // Marker tracking for timestamp markers + private int markerCount = 0; + private float currentHeading = 0.0f; // Current bearing/heading in degrees + private float displayHeading = 0.0f; // Smoothed heading for UI arrow + + // Parameters for heading smoothing and disturbance handling. + private static final float DISPLAY_HEADING_DEADBAND_DEG = 10f; + private static final float DISPLAY_HEADING_MAX_STEP_DEG = 10.0f; + private static final float DISPLAY_HEADING_ALPHA_NORMAL = 0.40f; + private static final float DISPLAY_HEADING_ALPHA_DISTURBED = 0.16f; + private static final float DISPLAY_HEADING_MOVE_FUSION_WEIGHT = 0.20f; + private static final float DISPLAY_HEADING_MOVE_FUSION_WEIGHT_DISTURBED = 0.35f; + private static final float HEADING_MOVE_MIN_DISTANCE_METERS = 0.12f; + private static final float MAGNETIC_FIELD_MIN_UT = 20.0f; + private static final float MAGNETIC_FIELD_MAX_UT = 60.0f; + + private boolean displayHeadingInitialized = false; + private boolean hasLastPdrForHeading = false; + private float lastPdrXForHeading = 0.0f; + private float lastPdrYForHeading = 0.0f; + private long lastGyroTimestampNs = 0L; + private float gyroHeadingDeg = 0.0f; + private boolean gyroHeadingInitialized = false; + // Sensor values - private float[] acceleration; - private float[] filteredAcc; - private float[] gravity; - private float[] magneticField; - private float[] angularVelocity; - private float[] orientation; - private float[] rotation; - private float pressure; + private float[] acceleration = new float[3]; + private float[] filteredAcc = new float[3]; + private float[] gravity = new float[3]; + private float[] magneticField = new float[3]; + + private float[] angularVelocity = new float[3]; + private float[] orientation = new float[3]; + private float[] rotation = new float[4]; // x, y, z, w + private final float[] remappedRotationMatrix = new float[9]; + private float pressure = Float.NaN; private float light; private float proximity; - private float[] R; - private int stepCounter ; + private int stepCounter; + + // Recording statistics + private int gnssRecordCount = 0; + private int pdrRecordCount = 0; + // Derived values private float elevation; private boolean elevator; - // Location values private float latitude; private float longitude; - private float[] startLocation; - // Wifi values - private List wifiList; - - - // Over time accelerometer magnitude values since last step - private List accelMagnitude; + private float altitude_val = Float.NaN; + private float gnssAccuracy = Float.MAX_VALUE; // GNSS accuracy in meters + private float gnssVerticalAccuracy = Float.MAX_VALUE; + private float barometerAbsoluteAltitude = Float.NaN; + private float smoothedBarometerAbsoluteAltitude = Float.NaN; + private float smoothedGnssAltitude = Float.NaN; + private float estimatedAbsoluteAltitude = Float.NaN; + private float altitudeOffsetMeters = Float.NaN; + private float seaLevelPressure = Float.NaN; + private boolean hasAltitudeCalibration = false; + private float latestLinearAccelerationMagnitude = 0f; + private float latestSpeedMetersPerSecond = 0f; + private int lastStableEstimatedFloor = Integer.MIN_VALUE; + private boolean barometerAutoFloorEnabled = false; + private float[] startLocation = new float[2]; + private LatLng lastWifiCandidateLocation = null; + private LatLng lastAcceptedWifiLocation = null; + private int stableWifiCandidateCount = 0; + + private List wifiList = new ArrayList<>(); + private List bleList = new ArrayList<>(); + private List accelMagnitude = new ArrayList<>(); - // PDR calculation class private PdrProcessing pdrProcessing; - - // Trajectory displaying class private PathView pathView; - // WiFi positioning object private WiFiPositioning wiFiPositioning; - - //region Initialisation - /** - * Private constructor for implementing singleton design pattern for SensorFusion. - * Initialises empty arrays and new objects that do not depends on outside information. - */ + private CircularFloatBuffer barometerAltitudeWindow; + private CircularFloatBuffer gnssAltitudeWindow; + private CircularFloatBuffer motionWindow; + + // Rolling window state for acceleration smoothing. + private static final int SMOOTH_WINDOW = 20; + private float[] accWindow = new float[SMOOTH_WINDOW]; + private int accWindowIndex = 0; + + // Dynamic stride estimation state (Weinberg method). + private float currentMaxAcc = 0; + private float currentMinAcc = Float.MAX_VALUE; + private long lastEnhancedStepTimeMs = 0L; + private static final long MIN_STEP_DELAY_MS = 300; + private static final long STEP_DETECTOR_FALLBACK_GUARD_MS = 420; + private static final float STEP_THRESHOLD = 1.5f; + private static final float STEP_AMPLITUDE_THRESHOLD = 1.1f; + private static final float MIN_DYNAMIC_STRIDE_METERS = 0.30f; + private static final float MAX_DYNAMIC_STRIDE_METERS = 0.85f; + + // region Initialisation private SensorFusion() { - // Location listener to be used by the GNSS class this.locationListener= new myLocationListener(); - // Timer to store sensor values in the trajectory object this.storeTrajectoryTimer = new Timer(); - // Counters to track elements with slower frequency this.counter = 0; this.secondCounter = 0; - // Step count initial value this.stepCounter = 0; - // PDR elevation initial values this.elevation = 0; this.elevator = false; - // PDR position array - this.startLocation = new float[2]; - // Empty array initialisation - this.acceleration = new float[3]; - this.filteredAcc = new float[3]; - this.gravity = new float[3]; - this.magneticField = new float[3]; - this.angularVelocity = new float[3]; - this.orientation = new float[3]; - this.rotation = new float[4]; this.rotation[3] = 1.0f; - this.R = new float[9]; - // GNSS initial Long-Lat array - this.startLocation = new float[2]; + this.wifiList = new ArrayList<>(); + this.bleList = new ArrayList<>(); + this.barometerAltitudeWindow = new CircularFloatBuffer(BAROMETER_ALTITUDE_WINDOW); + this.gnssAltitudeWindow = new CircularFloatBuffer(GNSS_ALTITUDE_WINDOW); + this.motionWindow = new CircularFloatBuffer(MOTION_WINDOW); } - - /** - * Static function to access singleton instance of SensorFusion. - * - * @return singleton instance of SensorFusion class. - */ public static SensorFusion getInstance() { return sensorFusion; } - /** - * Initialisation function for the SensorFusion instance. - * - * Initialise all Movement sensor instances from context and predetermined types. Creates a - * server communication instance for sending trajectories. Saves current absolute and relative - * time, and initialises saving the recording to false. - * - * @param context application context for permissions and device access. - * - * @see MovementSensor handling all SensorManager based data collection devices. - * @see ServerCommunications handling communication with the server. - * @see GNSSDataProcessor for location data processing. - * @see WifiDataProcessor for network data processing. - */ + public void setWifiApiAuthToken(String token) { + if (wiFiPositioning != null) { + wiFiPositioning.setApiAuthToken(token); + } + } + public void setContext(Context context) { - this.appContext = context.getApplicationContext(); // store app context for later use + this.appContext = context.getApplicationContext(); - // Initialise data collection devices (unchanged)... this.accelerometerSensor = new MovementSensor(context, Sensor.TYPE_ACCELEROMETER); this.barometerSensor = new MovementSensor(context, Sensor.TYPE_PRESSURE); this.gyroscopeSensor = new MovementSensor(context, Sensor.TYPE_GYROSCOPE); @@ -231,122 +280,114 @@ public void setContext(Context context) { this.rotationSensor = new MovementSensor(context, Sensor.TYPE_ROTATION_VECTOR); this.gravitySensor = new MovementSensor(context, Sensor.TYPE_GRAVITY); this.linearAccelerationSensor = new MovementSensor(context, Sensor.TYPE_LINEAR_ACCELERATION); - // Listener based devices + this.wifiProcessor = new WifiDataProcessor(context); wifiProcessor.registerObserver(this); this.gnssProcessor = new GNSSDataProcessor(context, locationListener); - // Create object handling HTTPS communication + + BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); + if (bluetoothManager != null) { + this.bluetoothAdapter = bluetoothManager.getAdapter(); + } + this.serverCommunications = new ServerCommunications(context); - // Save absolute and relative start time this.absoluteStartTime = System.currentTimeMillis(); this.bootTime = SystemClock.uptimeMillis(); - // Initialise saveRecording to false this.saveRecording = false; - // Other initialisations... this.accelMagnitude = new ArrayList<>(); this.pdrProcessing = new PdrProcessing(context); this.settings = PreferenceManager.getDefaultSharedPreferences(context); this.pathView = new PathView(context, null); this.wiFiPositioning = new WiFiPositioning(context); + String openPositioningToken = settings.getString("openpositioning_api_token", + BuildConfig.OPENPOSITIONING_API_KEY); + this.wiFiPositioning.setApiAuthToken(openPositioningToken); + if(settings.getBoolean("overwrite_constants", false)) { this.filter_coefficient = Float.parseFloat(settings.getString("accel_filter", "0.96")); } else { this.filter_coefficient = FILTER_COEFFICIENT; } - // Keep app awake during the recording (using stored appContext) PowerManager powerManager = (PowerManager) this.appContext.getSystemService(Context.POWER_SERVICE); - wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp::MyWakelockTag"); - } + wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "PositionMe::WakeLock"); - //endregion - - //region Sensor processing - /** - * {@inheritDoc} - * - * Called every time a Sensor value is updated. - * - * Checks originating sensor type, if the data is meaningful save it to a local variable. - * - * @param sensorEvent SensorEvent of sensor with values changed, includes types and values. - */ + // Start BLE scanning for UI display + startBleScan(); + } + // endregion + + // region Sensor processing + // Handles sensor callbacks and updates fusion state in real time. + // Data flow by sensor type: + // Accelerometer/Linear Acceleration/Gravity: motion features for step and elevator logic. + // Rotation Vector + Gyroscope + Magnetometer: heading estimation and smoothing. + // Pressure: barometer altitude and floor estimation. + // Step Detector: step-triggered PDR update and optional trajectory write. @Override public void onSensorChanged(SensorEvent sensorEvent) { - long currentTime = System.currentTimeMillis(); // Current time in milliseconds + long currentTime = System.currentTimeMillis(); int sensorType = sensorEvent.sensor.getType(); - // Get the previous timestamp for this sensor type - Long lastTimestamp = lastEventTimestamps.get(sensorType); - - if (lastTimestamp != null) { - long timeGap = currentTime - lastTimestamp; - -// // Log a warning if the time gap is larger than the threshold -// if (timeGap > LARGE_GAP_THRESHOLD_MS) { -// Log.e("SensorFusion", "Large time gap detected for sensor " + sensorType + -// " | Time gap: " + timeGap + " ms"); -// } - } - - // Update timestamp and frequency counter for this sensor lastEventTimestamps.put(sensorType, currentTime); eventCounts.put(sensorType, eventCounts.getOrDefault(sensorType, 0) + 1); - - switch (sensorType) { case Sensor.TYPE_ACCELEROMETER: - acceleration[0] = sensorEvent.values[0]; - acceleration[1] = sensorEvent.values[1]; - acceleration[2] = sensorEvent.values[2]; + System.arraycopy(sensorEvent.values, 0, acceleration, 0, 3); + + double accelMag = Math.sqrt( + acceleration[0] * acceleration[0] + + acceleration[1] * acceleration[1] + + acceleration[2] * acceleration[2] + ); + this.accelMagnitude.add(accelMag); + + // Use accelerometer samples for stride-based PDR update. + if (saveRecording) { + updateEnhancedPDR(acceleration[0], acceleration[1], acceleration[2]); + } break; case Sensor.TYPE_PRESSURE: - pressure = (1 - ALPHA) * pressure + ALPHA * sensorEvent.values[0]; - if (saveRecording) { - this.elevation = pdrProcessing.updateElevation( - SensorManager.getAltitude(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, pressure) - ); + if (Float.isNaN(pressure)) { + pressure = sensorEvent.values[0]; + } else { + pressure = (1 - ALPHA) * pressure + ALPHA * sensorEvent.values[0]; } + updateBarometerAbsoluteAltitude(); + smoothedBarometerAbsoluteAltitude = smoothAltitudeSample( + barometerAltitudeWindow, + barometerAbsoluteAltitude + ); + // Keep elevation state live so floor estimation works in non-recording mode too. + this.elevation = pdrProcessing.updateElevation( + smoothedBarometerAbsoluteAltitude + ); + updateAbsoluteAltitudeEstimate(); break; case Sensor.TYPE_GYROSCOPE: - angularVelocity[0] = sensorEvent.values[0]; - angularVelocity[1] = sensorEvent.values[1]; - angularVelocity[2] = sensorEvent.values[2]; + System.arraycopy(sensorEvent.values, 0, angularVelocity, 0, 3); + updateGyroHeading(sensorEvent); + break; case Sensor.TYPE_LINEAR_ACCELERATION: - filteredAcc[0] = sensorEvent.values[0]; - filteredAcc[1] = sensorEvent.values[1]; - filteredAcc[2] = sensorEvent.values[2]; - - // Compute magnitude & add to accelMagnitude + System.arraycopy(sensorEvent.values, 0, filteredAcc, 0, 3); double accelMagFiltered = Math.sqrt( - Math.pow(filteredAcc[0], 2) + - Math.pow(filteredAcc[1], 2) + - Math.pow(filteredAcc[2], 2) + filteredAcc[0]*filteredAcc[0] + filteredAcc[1]*filteredAcc[1] + filteredAcc[2]*filteredAcc[2] ); - this.accelMagnitude.add(accelMagFiltered); - -// // Debug logging -// Log.v("SensorFusion", -// "Added new linear accel magnitude: " + accelMagFiltered -// + "; accelMagnitude size = " + accelMagnitude.size()); - + latestLinearAccelerationMagnitude = (float) accelMagFiltered; + motionWindow.putNewest(latestLinearAccelerationMagnitude); + elevator = pdrProcessing.estimateElevator(gravity, filteredAcc); + refreshFusionMotionState(); break; case Sensor.TYPE_GRAVITY: - gravity[0] = sensorEvent.values[0]; - gravity[1] = sensorEvent.values[1]; - gravity[2] = sensorEvent.values[2]; - - // Possibly log gravity values if needed - //Log.v("SensorFusion", "Gravity: " + Arrays.toString(gravity)); - + System.arraycopy(sensorEvent.values, 0, gravity, 0, 3); elevator = pdrProcessing.estimateElevator(gravity, filteredAcc); break; @@ -359,468 +400,805 @@ public void onSensorChanged(SensorEvent sensorEvent) { break; case Sensor.TYPE_MAGNETIC_FIELD: - magneticField[0] = sensorEvent.values[0]; - magneticField[1] = sensorEvent.values[1]; - magneticField[2] = sensorEvent.values[2]; + System.arraycopy(sensorEvent.values, 0, magneticField, 0, 3); break; case Sensor.TYPE_ROTATION_VECTOR: - this.rotation = sensorEvent.values.clone(); + if (sensorEvent.values.length >= 4) { + System.arraycopy(sensorEvent.values, 0, rotation, 0, 4); + } else { + System.arraycopy(sensorEvent.values, 0, rotation, 0, 3); + rotation[3] = 1.0f; + } float[] rotationVectorDCM = new float[9]; SensorManager.getRotationMatrixFromVector(rotationVectorDCM, this.rotation); - SensorManager.getOrientation(rotationVectorDCM, this.orientation); + applyDisplayCompensation(rotationVectorDCM, remappedRotationMatrix); + SensorManager.getOrientation(remappedRotationMatrix, this.orientation); + this.currentHeading = normalizeHeadingDegrees((float) Math.toDegrees(this.orientation[0])); + updateDisplayHeading(this.currentHeading); break; case Sensor.TYPE_STEP_DETECTOR: long stepTime = SystemClock.uptimeMillis() - bootTime; + if (saveRecording && (currentTime - lastEnhancedStepTimeMs) < STEP_DETECTOR_FALLBACK_GUARD_MS) { + break; + } - if (currentTime - lastStepTime < 20) { - Log.e("SensorFusion", "Ignoring step event, too soon after last step event:" + (currentTime - lastStepTime) + " ms"); - // Ignore rapid successive step events + if (currentTime - lastStepTime < 220) { break; } - else { - lastStepTime = currentTime; - // Log if accelMagnitude is empty - if (accelMagnitude.isEmpty()) { - Log.e("SensorFusion", - "stepDetection triggered, but accelMagnitude is empty! " + - "This can cause updatePdr(...) to fail or return bad results."); - } else { - Log.d("SensorFusion", - "stepDetection triggered, accelMagnitude size = " + accelMagnitude.size()); - } + lastStepTime = currentTime; - float[] newCords = this.pdrProcessing.updatePdr( - stepTime, - this.accelMagnitude, - this.orientation[0] - ); + float currentRegularHeading = (float) Math.toRadians(this.currentHeading); + float[] newCords = this.pdrProcessing.updatePdr( + stepTime, + this.accelMagnitude, + currentRegularHeading + ); + updateDisplayHeadingWithMovement(newCords[0], newCords[1]); - // Clear the accelMagnitude after using it - this.accelMagnitude.clear(); + this.accelMagnitude.clear(); + if (saveRecording && trajectory != null) { + this.pathView.drawTrajectory(newCords); + stepCounter++; + trajectory.addPdrData(Traj.RelativePosition.newBuilder() + .setRelativeTimestamp(stepTime) + .setX(newCords[0]) + .setY(newCords[1]) + .build()); - if (saveRecording) { - this.pathView.drawTrajectory(newCords); - stepCounter++; - trajectory.addPdrData(Traj.Pdr_Sample.newBuilder() - .setRelativeTimestamp(SystemClock.uptimeMillis() - bootTime) - .setX(newCords[0]) - .setY(newCords[1])); - } - break; + pdrRecordCount++; + Log.d("SensorFusion", "Step detector PDR recorded: count=" + pdrRecordCount + ", steps=" + stepCounter); } - + break; } } - /** - * Utility function to log the event frequency of each sensor. - * Call this periodically for debugging purposes. - */ - public void logSensorFrequencies() { - for (int sensorType : eventCounts.keySet()) { - Log.d("SensorFusion", "Sensor " + sensorType + " | Event Count: " + eventCounts.get(sensorType)); + // Smooth acceleration magnitude with a fixed-size moving average window. + private float getSmoothedMagnitude(float rawMagnitude) { + accWindow[accWindowIndex] = rawMagnitude; + accWindowIndex = (accWindowIndex + 1) % SMOOTH_WINDOW; + + float sum = 0; + for (float v : accWindow) sum += v; + return sum / SMOOTH_WINDOW; + } + + // Compute stride length from peak-to-peak acceleration amplitude. + private float calculateWeinbergStride(float aMax, float aMin) { + float K = 0.40f; + double amplitude = aMax - aMin; + if (amplitude < 0) amplitude = 0; + return (float) (K * Math.pow(amplitude, 0.25)); + } + + private float clampStride(float strideMeters) { + return Math.max(MIN_DYNAMIC_STRIDE_METERS, Math.min(MAX_DYNAMIC_STRIDE_METERS, strideMeters)); + } + + private void updateEnhancedPDR(float x, float y, float z) { + float rawMag = Math.abs((float) Math.sqrt(x*x + y*y + z*z) - 9.81f); + + // Suppress high-frequency jitter before threshold-based step detection. + float smoothMag = getSmoothedMagnitude(rawMag); + + // Maintain local extrema for stride estimation once a step is detected. + if (smoothMag > currentMaxAcc) currentMaxAcc = smoothMag; + if (smoothMag < currentMinAcc) currentMinAcc = smoothMag; + + // Trigger a new step only if threshold and minimum step interval are both satisfied. + long currentTime = System.currentTimeMillis(); + if (smoothMag > STEP_THRESHOLD && (currentTime - lastStepTime) > MIN_STEP_DELAY_MS) { + float peakToPeak = (currentMinAcc == Float.MAX_VALUE) ? 0f : (currentMaxAcc - currentMinAcc); + if (peakToPeak < STEP_AMPLITUDE_THRESHOLD) { + return; + } + + // Compute per-step stride length and feed it into PDR integration. + float dynamicStride = clampStride(calculateWeinbergStride(currentMaxAcc, currentMinAcc)); + + // Use current fused heading for forward projection of this step. + float currentHeading = (float) Math.toRadians(this.currentHeading); + + long stepTime = SystemClock.uptimeMillis() - bootTime; + lastStepTime = currentTime; + lastEnhancedStepTimeMs = currentTime; + + float[] newCords = this.pdrProcessing.updatePdrWithStride( + dynamicStride, + currentHeading + ); + updateDisplayHeadingWithMovement(newCords[0], newCords[1]); + + // Reset extrema for the next step cycle. + currentMaxAcc = 0; + currentMinAcc = Float.MAX_VALUE; + + if (saveRecording && trajectory != null) { + this.pathView.drawTrajectory(newCords); + stepCounter++; + trajectory.addPdrData(Traj.RelativePosition.newBuilder() + .setRelativeTimestamp(stepTime) + .setX(newCords[0]) + .setY(newCords[1]) + .build()); + + pdrRecordCount++; + Log.d("Recording", "Enhanced PDR recorded: count=" + pdrRecordCount + + ", steps=" + stepCounter); + } } } - /** - * {@inheritDoc} - * - * Location listener class to receive updates from the location manager. - * - * Passed to the {@link GNSSDataProcessor} to receive the location data in this class. Save the - * values in instance variables. - */ class myLocationListener implements LocationListener{ @Override public void onLocationChanged(@NonNull Location location) { - //Toast.makeText(context, "Location Changed", Toast.LENGTH_SHORT).show(); latitude = (float) location.getLatitude(); longitude = (float) location.getLongitude(); - float altitude = (float) location.getAltitude(); - float accuracy = (float) location.getAccuracy(); - float speed = (float) location.getSpeed(); - String provider = location.getProvider(); - if(saveRecording) { - trajectory.addGnssData(Traj.GNSS_Sample.newBuilder() - .setAccuracy(accuracy) - .setAltitude(altitude) - .setLatitude(latitude) - .setLongitude(longitude) - .setSpeed(speed) - .setProvider(provider) - .setRelativeTimestamp(System.currentTimeMillis()-absoluteStartTime)); + altitude_val = (float) location.getAltitude(); + gnssAccuracy = location.getAccuracy(); + latestSpeedMetersPerSecond = location.hasSpeed() ? location.getSpeed() : 0f; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && location.hasVerticalAccuracy()) { + gnssVerticalAccuracy = location.getVerticalAccuracyMeters(); + } else { + gnssVerticalAccuracy = Math.max(gnssAccuracy * 1.5f, 10f); + } + + recalibrateAbsoluteAltitude(location); + + // Fusion origin is initialized from GNSS only in outdoor context. + if (!pdrProcessing.isFusionInitialized() && shouldUseGnssInitialization(latitude, longitude)) { + pdrProcessing.initializeWithLocation(latitude, longitude); + if (pdrProcessing.isFusionInitialized()) { + LatLng origin = pdrProcessing.getFusedLatLon(); + setStartGNSSLatitude(new float[]{(float) origin.latitude, (float) origin.longitude}); + } + } + + if (pdrProcessing.isFusionInitialized()) { + pdrProcessing.processGnssLocation(latitude, longitude, gnssAccuracy); + } + refreshFusionMotionState(); + + if(saveRecording && trajectory != null) { + long relativeTime = SystemClock.uptimeMillis() - bootTime; + + Traj.GNSSPosition gnssPosition = Traj.GNSSPosition.newBuilder() + .setLatitude(location.getLatitude()) + .setLongitude(location.getLongitude()) + .setAltitude(location.getAltitude()) + .setRelativeTimestamp(relativeTime) + .build(); + + trajectory.addGnssData(Traj.GNSSReading.newBuilder() + .setPosition(gnssPosition) + .setAccuracy(location.getAccuracy()) + .setSpeed(location.getSpeed()) + .setBearing(location.getBearing()) + .setProvider(location.getProvider() != null ? location.getProvider() : "unknown") + .build()); + + gnssRecordCount++; + Log.d("Recording", "GNSS recorded: count=" + gnssRecordCount + + ", lat=" + location.getLatitude() + + ", lon=" + location.getLongitude() + + ", accuracy=" + location.getAccuracy() + "m"); + } + } + } + + private void recalibrateAbsoluteAltitude(@NonNull Location location) { + if (!location.hasAltitude() || Float.isNaN(pressure) || Float.isNaN(smoothedBarometerAbsoluteAltitude)) { + return; + } + + smoothedGnssAltitude = smoothAltitudeSample(gnssAltitudeWindow, (float) location.getAltitude()); + + boolean goodVerticalFix = gnssVerticalAccuracy <= ACCEPTABLE_VERTICAL_ACCURACY_METERS; + boolean acceptableHorizontalFix = gnssAccuracy <= ACCEPTABLE_HORIZONTAL_ACCURACY_METERS; + if (!goodVerticalFix || !acceptableHorizontalFix) { + return; + } + + boolean stationary = isAltitudeCalibrationStationary(); + if (!stationary && gnssVerticalAccuracy > TRUSTED_VERTICAL_ACCURACY_METERS) { + return; + } + + float measuredSeaLevelPressure = estimateSeaLevelPressure(pressure, smoothedGnssAltitude); + if (Float.isNaN(measuredSeaLevelPressure)) { + return; + } + + if (Float.isNaN(seaLevelPressure)) { + seaLevelPressure = measuredSeaLevelPressure; + } else { + float seaLevelGain = stationary ? SEA_LEVEL_GAIN_STATIONARY : SEA_LEVEL_GAIN_MOVING; + if (isInFastAltitudeConvergenceWindow()) { + seaLevelGain = Math.max(seaLevelGain, SEA_LEVEL_GAIN_FAST); } + seaLevelPressure = seaLevelPressure + seaLevelGain * (measuredSeaLevelPressure - seaLevelPressure); + } + + updateBarometerAbsoluteAltitude(); + smoothedBarometerAbsoluteAltitude = smoothAltitudeSample( + barometerAltitudeWindow, + barometerAbsoluteAltitude + ); + + float measuredOffset = smoothedGnssAltitude - smoothedBarometerAbsoluteAltitude; + if (hasAltitudeCalibration && Math.abs(measuredOffset - altitudeOffsetMeters) > MAX_ALTITUDE_OFFSET_JUMP_METERS && !stationary) { + return; + } + + if (!hasAltitudeCalibration || Float.isNaN(altitudeOffsetMeters)) { + altitudeOffsetMeters = measuredOffset; + hasAltitudeCalibration = true; + } else { + float calibrationGain = stationary ? OFFSET_CALIBRATION_GAIN_STATIONARY : OFFSET_CALIBRATION_GAIN_MOVING; + if (isInFastAltitudeConvergenceWindow()) { + calibrationGain = Math.max(calibrationGain, OFFSET_CALIBRATION_GAIN_FAST); + } + altitudeOffsetMeters = altitudeOffsetMeters + calibrationGain * (measuredOffset - altitudeOffsetMeters); + } + + updateAbsoluteAltitudeEstimate(); + } + + private void updateBarometerAbsoluteAltitude() { + if (Float.isNaN(pressure)) { + return; + } + + float referenceSeaLevelPressure = !Float.isNaN(seaLevelPressure) + ? seaLevelPressure + : SensorManager.PRESSURE_STANDARD_ATMOSPHERE; + barometerAbsoluteAltitude = SensorManager.getAltitude(referenceSeaLevelPressure, pressure); + } + + private float estimateSeaLevelPressure(float measuredPressure, float knownAltitudeMeters) { + if (Float.isNaN(measuredPressure) || Float.isNaN(knownAltitudeMeters)) { + return Float.NaN; + } + + float normalizedAltitude = 1.0f - (knownAltitudeMeters / 44330.0f); + if (normalizedAltitude <= 0f) { + return Float.NaN; + } + + float estimatedPressure = (float) (measuredPressure / Math.pow(normalizedAltitude, 5.255d)); + if (estimatedPressure < MIN_SEA_LEVEL_PRESSURE_HPA || estimatedPressure > MAX_SEA_LEVEL_PRESSURE_HPA) { + return Float.NaN; } + + return estimatedPressure; + } + + private void updateAbsoluteAltitudeEstimate() { + float targetAltitude = Float.NaN; + + if (!Float.isNaN(smoothedBarometerAbsoluteAltitude) && hasAltitudeCalibration && !Float.isNaN(altitudeOffsetMeters)) { + targetAltitude = smoothedBarometerAbsoluteAltitude + altitudeOffsetMeters; + } else if (!Float.isNaN(smoothedGnssAltitude)) { + targetAltitude = smoothedGnssAltitude; + } else if (!Float.isNaN(altitude_val)) { + targetAltitude = altitude_val; + } + + if (Float.isNaN(targetAltitude)) { + return; + } + + if (Float.isNaN(estimatedAbsoluteAltitude)) { + estimatedAbsoluteAltitude = targetAltitude; + } else { + float smoothingGain = isAltitudeCalibrationStationary() + ? ALTITUDE_SMOOTHING_GAIN_STATIONARY + : ALTITUDE_SMOOTHING_GAIN_MOVING; + if (isInFastAltitudeConvergenceWindow()) { + smoothingGain = Math.max(smoothingGain, ALTITUDE_SMOOTHING_GAIN_FAST); + } + estimatedAbsoluteAltitude = estimatedAbsoluteAltitude + smoothingGain * (targetAltitude - estimatedAbsoluteAltitude); + } + } + + private boolean isInFastAltitudeConvergenceWindow() { + return (SystemClock.uptimeMillis() - bootTime) <= ALTITUDE_FAST_CONVERGENCE_WINDOW_MS; + } + + private float smoothAltitudeSample(@NonNull CircularFloatBuffer buffer, float sample) { + buffer.putNewest(sample); + List samples = buffer.getListCopy(); + if (samples == null || samples.isEmpty()) { + return sample; + } + + float sum = 0f; + for (float value : samples) { + sum += value; + } + return sum / samples.size(); + } + + private boolean isAltitudeCalibrationStationary() { + List motionSamples = motionWindow.getListCopy(); + float averageMotion = latestLinearAccelerationMagnitude; + if (motionSamples != null && !motionSamples.isEmpty()) { + float sum = 0f; + for (float sample : motionSamples) { + sum += Math.abs(sample); + } + averageMotion = sum / motionSamples.size(); + } + + long timeSinceLastStepMs = lastStepTime == 0 ? Long.MAX_VALUE : (System.currentTimeMillis() - lastStepTime); + boolean lowMotion = averageMotion < STATIONARY_ACCEL_THRESHOLD; + boolean lowSpeed = latestSpeedMetersPerSecond <= STATIONARY_SPEED_THRESHOLD; + return lowMotion && lowSpeed && timeSinceLastStepMs >= STATIONARY_STEP_GAP_MS; + } + + private void refreshFusionMotionState() { + if (pdrProcessing == null) { + return; + } + + pdrProcessing.setMotionState( + isAltitudeCalibrationStationary(), + latestSpeedMetersPerSecond, + latestLinearAccelerationMagnitude + ); } - /** - * {@inheritDoc} - * - * Receives updates from {@link WifiDataProcessor}. - * - * @see WifiDataProcessor object for wifi scanning. - */ @Override public void update(Object[] wifiList) { - // Save newest wifi values to local variable this.wifiList = Stream.of(wifiList).map(o -> (Wifi) o).collect(Collectors.toList()); - if(this.saveRecording) { - Traj.WiFi_Sample.Builder wifiData = Traj.WiFi_Sample.newBuilder() - .setRelativeTimestamp(SystemClock.uptimeMillis()-bootTime); + if(this.saveRecording && trajectory != null) { + long relativeTime = SystemClock.uptimeMillis() - bootTime; + Traj.GNSSPosition replayWifiPosition = buildReplayPositionForWifi(relativeTime); + + Traj.Fingerprint.Builder fingerprintBuilder = Traj.Fingerprint.newBuilder() + .setRelativeTimestamp(relativeTime); + for (Wifi data : this.wifiList) { - wifiData.addMacScans(Traj.Mac_Scan.newBuilder() - .setRelativeTimestamp(SystemClock.uptimeMillis() - bootTime) - .setMac(data.getBssid()).setRssi(data.getLevel())); + Traj.RFScan.Builder rfBuilder = Traj.RFScan.newBuilder() + .setRelativeTimestamp(relativeTime) + .setMac(data.getBssid()) + .setRssi(data.getLevel()); + + if (replayWifiPosition != null) { + rfBuilder.setPosition(replayWifiPosition); + } + + fingerprintBuilder.addRfScans(rfBuilder.build()); } - // Adding WiFi data to Trajectory - this.trajectory.addWifiData(wifiData); + + this.trajectory.addWifiFingerprints(fingerprintBuilder.build()); } createWifiPositioningRequest(); } - /** - * Function to create a request to obtain a wifi location for the obtained wifi fingerprint - * - */ - private void createWifiPositioningRequest(){ - // Try catch block to catch any errors and prevent app crashing - try { - // Creating a JSON object to store the WiFi access points - JSONObject wifiAccessPoints=new JSONObject(); - for (Wifi data : this.wifiList){ - wifiAccessPoints.put(String.valueOf(data.getBssid()), data.getLevel()); + private Traj.GNSSPosition buildReplayPositionForWifi(long relativeTime) { + // Replay WiFi trace must reflect WiFi positioning output only. + // Do not fallback to fused/GNSS here, otherwise WiFi appears to follow GNSS/PDR. + LatLng wifiLocation = wiFiPositioning != null ? wiFiPositioning.getWifiLocation() : null; + if (wifiLocation != null && isValidCoordinate(wifiLocation.latitude, wifiLocation.longitude)) { + return Traj.GNSSPosition.newBuilder() + .setLatitude(wifiLocation.latitude) + .setLongitude(wifiLocation.longitude) + .setAltitude(altitude_val) + .setRelativeTimestamp(relativeTime) + .build(); + } + + if (lastAcceptedWifiLocation != null + && isValidCoordinate(lastAcceptedWifiLocation.latitude, lastAcceptedWifiLocation.longitude)) { + return Traj.GNSSPosition.newBuilder() + .setLatitude(lastAcceptedWifiLocation.latitude) + .setLongitude(lastAcceptedWifiLocation.longitude) + .setAltitude(altitude_val) + .setRelativeTimestamp(relativeTime) + .build(); + } + + return null; + } + + private boolean isValidCoordinate(double lat, double lon) { + if (Double.isNaN(lat) || Double.isNaN(lon)) { + return false; + } + if (lat < -90.0 || lat > 90.0 || lon < -180.0 || lon > 180.0) { + return false; + } + return !(Math.abs(lat) < 1e-8 && Math.abs(lon) < 1e-8); + } + + private boolean shouldFuseWifiLocation(@NonNull LatLng wifiLocation) { + boolean stationary = isAltitudeCalibrationStationary(); + + if (lastWifiCandidateLocation != null + && GeometryUtils.distanceBetween(lastWifiCandidateLocation, wifiLocation) <= WIFI_STABLE_DISTANCE_METERS) { + stableWifiCandidateCount++; + } else { + stableWifiCandidateCount = 1; + } + lastWifiCandidateLocation = wifiLocation; + + int requiredStableCount = stationary + ? WIFI_STABLE_CONFIRMATIONS_STATIONARY + : WIFI_STABLE_CONFIRMATIONS_MOVING; + if (stableWifiCandidateCount < requiredStableCount) { + return false; + } + + if (stationary) { + LatLng fusedLocation = pdrProcessing != null ? pdrProcessing.getFusedLatLon() : null; + if (fusedLocation != null + && GeometryUtils.distanceBetween(fusedLocation, wifiLocation) > WIFI_STATIONARY_FUSED_GATE_METERS) { + return false; + } + + if (lastAcceptedWifiLocation != null + && GeometryUtils.distanceBetween(lastAcceptedWifiLocation, wifiLocation) > WIFI_STATIONARY_ACCEPTED_GATE_METERS) { + return false; } - // Creating POST Request - JSONObject wifiFingerPrint = new JSONObject(); - wifiFingerPrint.put(WIFI_FINGERPRINT, wifiAccessPoints); - this.wiFiPositioning.request(wifiFingerPrint); - } catch (JSONException e) { - // Catching error while making JSON object, to prevent crashes - // Error log to keep record of errors (for secure programming and maintainability) - Log.e("jsonErrors","Error creating json object"+e.toString()); } + + lastAcceptedWifiLocation = wifiLocation; + return true; } - // Callback Example Function - /** - * Function to create a request to obtain a wifi location for the obtained wifi fingerprint - * using Volley Callback - */ - private void createWifiPositionRequestCallback(){ + + private void createWifiPositioningRequest(){ try { - // Creating a JSON object to store the WiFi access points JSONObject wifiAccessPoints=new JSONObject(); for (Wifi data : this.wifiList){ wifiAccessPoints.put(String.valueOf(data.getBssid()), data.getLevel()); } - // Creating POST Request JSONObject wifiFingerPrint = new JSONObject(); wifiFingerPrint.put(WIFI_FINGERPRINT, wifiAccessPoints); this.wiFiPositioning.request(wifiFingerPrint, new WiFiPositioning.VolleyCallback() { @Override - public void onSuccess(LatLng wifiLocation, int floor) { - // Handle the success response + public void onSuccess(LatLng location, int floor) { + refreshFusionMotionState(); + if (location != null && shouldFuseWifiLocation(location)) { + boolean fusionWasInitialized = pdrProcessing.isFusionInitialized(); + if (fusionWasInitialized || shouldUseWifiInitialization(location)) { + pdrProcessing.processWifiLocation(location, floor); + if (!fusionWasInitialized && pdrProcessing.isFusionInitialized()) { + LatLng origin = pdrProcessing.getFusedLatLon(); + setStartGNSSLatitude(new float[]{(float) origin.latitude, (float) origin.longitude}); + } + } + } } @Override public void onError(String message) { - // Handle the error response + Log.d("SensorFusion", "WiFi positioning skipped: " + message); } }); } catch (JSONException e) { - // Catching error while making JSON object, to prevent crashes - // Error log to keep record of errors (for secure programming and maintainability) Log.e("jsonErrors","Error creating json object"+e.toString()); } - } - /** - * Method to get user position obtained using {@link WiFiPositioning}. - * - * @return {@link LatLng} corresponding to user's position. - */ public LatLng getLatLngWifiPositioning(){return this.wiFiPositioning.getWifiLocation();} - /** - * Method to get current floor the user is at, obtained using WiFiPositioning - * @see WiFiPositioning for WiFi positioning - * @return Current floor user is at using WiFiPositioning - */ + private boolean isInsideKnownIndoorBuildings(double lat, double lon) { + LatLng point = new LatLng(lat, lon); + return BuildingPolygon.inAnyKnownBuilding(point); + } + + private boolean shouldUseWifiInitialization(@NonNull LatLng wifiLocation) { + return isInsideKnownIndoorBuildings(wifiLocation.latitude, wifiLocation.longitude); + } + + private boolean shouldUseGnssInitialization(float lat, float lon) { + return !isInsideKnownIndoorBuildings(lat, lon); + } + public int getWifiFloor(){ return this.wiFiPositioning.getFloor(); } - /** - * Method used for converting an array of orientation angles into a rotation matrix. - * - * @param o An array containing orientation angles in radians - * @return resultMatrix representing the orientation angles - */ - private float[] getRotationMatrixFromOrientation(float[] o) { - float[] xM = new float[9]; - float[] yM = new float[9]; - float[] zM = new float[9]; - - float sinX = (float)Math.sin(o[1]); - float cosX = (float)Math.cos(o[1]); - float sinY = (float)Math.sin(o[2]); - float cosY = (float)Math.cos(o[2]); - float sinZ = (float)Math.sin(o[0]); - float cosZ = (float)Math.cos(o[0]); - - // rotation about x-axis (pitch) - xM[0] = 1.0f; xM[1] = 0.0f; xM[2] = 0.0f; - xM[3] = 0.0f; xM[4] = cosX; xM[5] = sinX; - xM[6] = 0.0f; xM[7] = -sinX; xM[8] = cosX; - - // rotation about y-axis (roll) - yM[0] = cosY; yM[1] = 0.0f; yM[2] = sinY; - yM[3] = 0.0f; yM[4] = 1.0f; yM[5] = 0.0f; - yM[6] = -sinY; yM[7] = 0.0f; yM[8] = cosY; - - // rotation about z-axis (azimuth) - zM[0] = cosZ; zM[1] = sinZ; zM[2] = 0.0f; - zM[3] = -sinZ; zM[4] = cosZ; zM[5] = 0.0f; - zM[6] = 0.0f; zM[7] = 0.0f; zM[8] = 1.0f; - - // rotation order is y, x, z (roll, pitch, azimuth) - float[] resultMatrix = matrixMultiplication(xM, yM); - resultMatrix = matrixMultiplication(zM, resultMatrix); - return resultMatrix; - } - - /** - * Performs and matrix multiplication of two 3x3 matrices and returns the product. - * - * @param A An array representing a 3x3 matrix - * @param B An array representing a 3x3 matrix - * @return result representing the product of A and B - */ - private float[] matrixMultiplication(float[] A, float[] B) { - float[] result = new float[9]; - - result[0] = A[0] * B[0] + A[1] * B[3] + A[2] * B[6]; - result[1] = A[0] * B[1] + A[1] * B[4] + A[2] * B[7]; - result[2] = A[0] * B[2] + A[1] * B[5] + A[2] * B[8]; - - result[3] = A[3] * B[0] + A[4] * B[3] + A[5] * B[6]; - result[4] = A[3] * B[1] + A[4] * B[4] + A[5] * B[7]; - result[5] = A[3] * B[2] + A[4] * B[5] + A[5] * B[8]; - - result[6] = A[6] * B[0] + A[7] * B[3] + A[8] * B[6]; - result[7] = A[6] * B[1] + A[7] * B[4] + A[8] * B[7]; - result[8] = A[6] * B[2] + A[7] * B[5] + A[8] * B[8]; - - return result; - } - - /** - * {@inheritDoc} - */ - @Override - public void onAccuracyChanged(Sensor sensor, int i) {} - //endregion - - //region Getters/Setters - /** - * Getter function for core location data. - * - * @param start set true to get the initial location - * @return longitude and latitude data in a float[2]. - */ - public float[] getGNSSLatitude(boolean start) { - float [] latLong = new float[2]; - if(!start) { - latLong[0] = latitude; - latLong[1] = longitude; + // region Helper Methods + // Convert Android float[3] vectors to protobuf Vector3. + private Traj.Vector3 toVector3(float[] values) { + return Traj.Vector3.newBuilder() + .setX(values[0]) + .setY(values[1]) + .setZ(values[2]) + .build(); + } + + // Convert Android rotation vector to protobuf Quaternion. + private Traj.Quaternion toQuaternion(float[] values) { + return Traj.Quaternion.newBuilder() + .setX(values[0]) + .setY(values[1]) + .setZ(values[2]) + .setW(values.length > 3 ? values[3] : 1.0f) + .build(); + } + + // Serialize sensor metadata for trajectory headers. + private Traj.SensorInfo createSensorInfo(MovementSensor sensor) { + if (sensor == null || sensor.sensorInfo == null) return Traj.SensorInfo.getDefaultInstance(); + return Traj.SensorInfo.newBuilder() + .setName(sensor.sensorInfo.getName()) + .setVendor(sensor.sensorInfo.getVendor()) + .setResolution(sensor.sensorInfo.getResolution()) + .setPower(sensor.sensorInfo.getPower()) + .setVersion(sensor.sensorInfo.getVersion()) + .setType(sensor.sensorInfo.getType()) + .build(); + } + // endregion + + public void addMarker() { + if (saveRecording && trajectory != null) { + long relativeTime = SystemClock.uptimeMillis() - bootTime; + + // Marker index and name are persisted for replay-side marker lookup. + Traj.TimestampMarker marker = Traj.TimestampMarker.newBuilder() + .setRelativeTimestamp(relativeTime) + .setLatitude(latitude) + .setLongitude(longitude) + .setAltitude(altitude_val) + .setMarkerIndex(markerCount) + .setMarkerName("Marker_" + markerCount) + .build(); + + trajectory.addTestPoints(marker); + markerCount++; + Log.d("SensorFusion", "Marker #" + (markerCount - 1) + " added at: " + relativeTime + "ms"); } - else{ - latLong = startLocation; + } + + public void addMarkerAt(@NonNull LatLng markerLocation, float markerAltitude) { + if (!saveRecording || trajectory == null) { + return; } - return latLong; + + long relativeTime = SystemClock.uptimeMillis() - bootTime; + Traj.TimestampMarker marker = Traj.TimestampMarker.newBuilder() + .setRelativeTimestamp(relativeTime) + .setLatitude(markerLocation.latitude) + .setLongitude(markerLocation.longitude) + .setAltitude(markerAltitude) + .setMarkerIndex(markerCount) + .setMarkerName("Marker_" + markerCount) + .build(); + + trajectory.addTestPoints(marker); + markerCount++; + Log.d("SensorFusion", "Marker #" + (markerCount - 1) + " added at display location"); } - /** - * Setter function for core location data. - * - * @param startPosition contains the initial location set by the user - */ - public void setStartGNSSLatitude(float[] startPosition){ - startLocation = startPosition; + // Set the venue/building name for this trajectory. + public void setVenueName(String venueName) { + if (trajectory != null && venueName != null && !venueName.isEmpty()) { + trajectory.setVenueName(venueName); + Log.d("SensorFusion", "Venue name set to: " + venueName); + } } + // Set the building ID used by indoor map APIs. + public void setBuildingId(String buildingId) { + if (trajectory != null && buildingId != null && !buildingId.isEmpty()) { + trajectory.setBuildingId(buildingId); + Log.d("SensorFusion", "Building ID set to: " + buildingId); + } + } - /** - * Function to redraw path in corrections fragment. - * - * @param scalingRatio new size of path due to updated step length - */ - public void redrawPath(float scalingRatio){ - pathView.redraw(scalingRatio); + // Update current heading in degrees [0, 360). + public void updateHeading(float heading) { + this.currentHeading = normalizeHeadingDegrees(heading); } - /** - * Getter function for average step count. - * Calls the average step count function in pdrProcessing class - * - * @return average step count of total PDR. - */ - public float passAverageStepLength(){ - return pdrProcessing.getAverageStepLength(); + // Return current heading. + public float getCurrentHeading() { + return currentHeading; } - /** - * Getter function for device orientation. - * Passes the orientation variable - * - * @return orientation of device. - */ - public float passOrientation(){ - return orientation[0]; + public float getDisplayHeading() { + return displayHeadingInitialized ? displayHeading : currentHeading; } - /** - * Return most recent sensor readings. - * - * Collects all most recent readings from movement and location sensors, packages them in a map - * that is indexed by {@link SensorTypes} and makes it accessible for other classes. - * - * @return Map of SensorTypes to float array of most recent values. - */ - public Map getSensorValueMap() { - Map sensorValueMap = new HashMap<>(); - sensorValueMap.put(SensorTypes.ACCELEROMETER, acceleration); - sensorValueMap.put(SensorTypes.GRAVITY, gravity); - sensorValueMap.put(SensorTypes.MAGNETICFIELD, magneticField); - sensorValueMap.put(SensorTypes.GYRO, angularVelocity); - sensorValueMap.put(SensorTypes.LIGHT, new float[]{light}); - sensorValueMap.put(SensorTypes.PRESSURE, new float[]{pressure}); - sensorValueMap.put(SensorTypes.PROXIMITY, new float[]{proximity}); - sensorValueMap.put(SensorTypes.GNSSLATLONG, getGNSSLatitude(false)); - sensorValueMap.put(SensorTypes.PDR, pdrProcessing.getPDRMovement()); - return sensorValueMap; + private float normalizeHeadingDegrees(float headingDegrees) { + float normalized = headingDegrees % 360.0f; + if (normalized < 0) { + normalized += 360.0f; + } + return normalized; } - /** - * Return the most recent list of WiFi names and levels. - * Each Wifi object contains a BSSID and a level value. - * - * @return list of Wifi objects. - */ - public List getWifiList() { - return this.wifiList; - } - - /** - * Get information about all the sensors registered in SensorFusion. - * - * @return List of SensorInfo objects containing name, resolution, power, etc. - */ - public List getSensorInfos() { - List sensorInfoList = new ArrayList<>(); - sensorInfoList.add(this.accelerometerSensor.sensorInfo); - sensorInfoList.add(this.barometerSensor.sensorInfo); - sensorInfoList.add(this.gyroscopeSensor.sensorInfo); - sensorInfoList.add(this.lightSensor.sensorInfo); - sensorInfoList.add(this.proximitySensor.sensorInfo); - sensorInfoList.add(this.magnetometerSensor.sensorInfo); - return sensorInfoList; - } - - /** - * Registers the caller observer to receive updates from the server instance. - * Necessary when classes want to act on a trajectory being successfully or unsuccessfully send - * to the server. This grants access to observing the {@link ServerCommunications} instance - * used by the SensorFusion class. - * - * @param observer Instance implementing {@link Observer} class who wants to be notified of - * events relating to sending and receiving trajectories. - */ - public void registerForServerUpdate(Observer observer) { - serverCommunications.registerObserver(observer); + private void updateGyroHeading(SensorEvent sensorEvent) { + long timestampNs = sensorEvent.timestamp; + if (lastGyroTimestampNs == 0L) { + lastGyroTimestampNs = timestampNs; + return; + } + + float dtSeconds = (timestampNs - lastGyroTimestampNs) / 1_000_000_000.0f; + lastGyroTimestampNs = timestampNs; + if (dtSeconds <= 0.0f || dtSeconds > 0.2f) { + return; + } + + float yawRateRad = sensorEvent.values[2]; + float deltaYawDeg = (float) Math.toDegrees(yawRateRad * dtSeconds); + + if (!gyroHeadingInitialized) { + gyroHeadingDeg = currentHeading; + gyroHeadingInitialized = true; + } else { + gyroHeadingDeg = normalizeHeadingDegrees(gyroHeadingDeg + deltaYawDeg); + } + } + + private void updateDisplayHeading(float absoluteHeadingDeg) { + if (!displayHeadingInitialized) { + displayHeading = normalizeHeadingDegrees(absoluteHeadingDeg); + displayHeadingInitialized = true; + if (!gyroHeadingInitialized) { + gyroHeadingDeg = displayHeading; + gyroHeadingInitialized = true; + } + return; + } + + float magneticIntensity = (float) Math.sqrt( + magneticField[0] * magneticField[0] + + magneticField[1] * magneticField[1] + + magneticField[2] * magneticField[2]); + boolean magneticallyDisturbed = magneticIntensity < MAGNETIC_FIELD_MIN_UT + || magneticIntensity > MAGNETIC_FIELD_MAX_UT; + + float targetHeading = absoluteHeadingDeg; + if (magneticallyDisturbed && gyroHeadingInitialized) { + targetHeading = gyroHeadingDeg; + } + + float alpha = magneticallyDisturbed ? DISPLAY_HEADING_ALPHA_DISTURBED : DISPLAY_HEADING_ALPHA_NORMAL; + float diff = shortestAngleDiffDeg(displayHeading, targetHeading); + if (Math.abs(diff) <= DISPLAY_HEADING_DEADBAND_DEG) { + return; + } + + float limitedDiff = clamp(diff, -DISPLAY_HEADING_MAX_STEP_DEG, DISPLAY_HEADING_MAX_STEP_DEG); + displayHeading = normalizeHeadingDegrees(displayHeading + alpha * limitedDiff); + } + + private void updateDisplayHeadingWithMovement(float pdrX, float pdrY) { + if (!displayHeadingInitialized) { + return; + } + + if (!hasLastPdrForHeading) { + lastPdrXForHeading = pdrX; + lastPdrYForHeading = pdrY; + hasLastPdrForHeading = true; + return; + } + + float dx = pdrX - lastPdrXForHeading; + float dy = pdrY - lastPdrYForHeading; + lastPdrXForHeading = pdrX; + lastPdrYForHeading = pdrY; + + float distance = (float) Math.hypot(dx, dy); + if (distance < HEADING_MOVE_MIN_DISTANCE_METERS) { + return; + } + + float movementBearingDeg = normalizeHeadingDegrees((float) Math.toDegrees(Math.atan2(dx, dy))); + float magneticIntensity = (float) Math.sqrt( + magneticField[0] * magneticField[0] + + magneticField[1] * magneticField[1] + + magneticField[2] * magneticField[2]); + boolean magneticallyDisturbed = magneticIntensity < MAGNETIC_FIELD_MIN_UT + || magneticIntensity > MAGNETIC_FIELD_MAX_UT; + float weight = magneticallyDisturbed + ? DISPLAY_HEADING_MOVE_FUSION_WEIGHT_DISTURBED + : DISPLAY_HEADING_MOVE_FUSION_WEIGHT; + + float diff = shortestAngleDiffDeg(displayHeading, movementBearingDeg); + displayHeading = normalizeHeadingDegrees(displayHeading + weight * diff); } - /** - * Get the estimated elevation value in meters calculated by the PDR class. - * Elevation is relative to the starting position. - * - * @return float of the estimated elevation in meters. - */ - public float getElevation() { - return this.elevation; + private float shortestAngleDiffDeg(float fromDeg, float toDeg) { + float diff = normalizeHeadingDegrees(toDeg) - normalizeHeadingDegrees(fromDeg); + if (diff > 180.0f) { + diff -= 360.0f; + } else if (diff < -180.0f) { + diff += 360.0f; + } + return diff; } - /** - * Get an estimate by the PDR class whether it estimates the user is currently taking an elevator. - * - * @return true if the PDR estimates the user is in an elevator, false otherwise. - */ - public boolean getElevator() { - return this.elevator; + private float clamp(float value, float min, float max) { + if (value < min) { + return min; + } + if (value > max) { + return max; + } + return value; } - /** - * Estimates position of the phone based on proximity and light sensors. - * - * @return int 1 if the phone is by the ear, int 0 otherwise. - */ - public int getHoldMode(){ - int proximityThreshold = 1, lightThreshold = 100; //holdMode: by ear=1, not by ear =0 - if(proximitylightThreshold) { //unit cm - return 1; + // Compensate rotation matrix by screen orientation for consistent heading. + private void applyDisplayCompensation(float[] inR, float[] outR) { + int axisX = SensorManager.AXIS_X; + int axisY = SensorManager.AXIS_Y; + + int displayRotation = Surface.ROTATION_0; + try { + if (appContext != null) { + WindowManager windowManager = (WindowManager) appContext.getSystemService(Context.WINDOW_SERVICE); + if (windowManager != null && windowManager.getDefaultDisplay() != null) { + displayRotation = windowManager.getDefaultDisplay().getRotation(); + } + } + } catch (Exception ignored) { + displayRotation = Surface.ROTATION_0; } - else{ - return 0; + + switch (displayRotation) { + case Surface.ROTATION_90: + axisX = SensorManager.AXIS_Y; + axisY = SensorManager.AXIS_MINUS_X; + break; + case Surface.ROTATION_180: + axisX = SensorManager.AXIS_MINUS_X; + axisY = SensorManager.AXIS_MINUS_Y; + break; + case Surface.ROTATION_270: + axisX = SensorManager.AXIS_MINUS_Y; + axisY = SensorManager.AXIS_X; + break; + case Surface.ROTATION_0: + default: + axisX = SensorManager.AXIS_X; + axisY = SensorManager.AXIS_Y; + break; + } + + if (!SensorManager.remapCoordinateSystem(inR, axisX, axisY, outR)) { + System.arraycopy(inR, 0, outR, 0, 9); } } - //endregion + // Return current marker count. + public int getMarkerCount() { + return markerCount; + } - //region Start/Stop + // region Start/Stop - /** - * Registers all device listeners and enables updates with the specified sampling rate. - * - * Should be called from {@link MainActivity} when resuming the application. Sampling rate is in - * microseconds, IMU needs 100Hz, rest 1Hz - * - * @see MovementSensor handles SensorManager based devices. - * @see WifiDataProcessor handles wifi data. - * @see GNSSDataProcessor handles location data. - */ public void resumeListening() { - accelerometerSensor.sensorManager.registerListener(this, accelerometerSensor.sensor, 10000, (int) maxReportLatencyNs); - accelerometerSensor.sensorManager.registerListener(this, linearAccelerationSensor.sensor, 10000, (int) maxReportLatencyNs); - accelerometerSensor.sensorManager.registerListener(this, gravitySensor.sensor, 10000, (int) maxReportLatencyNs); - barometerSensor.sensorManager.registerListener(this, barometerSensor.sensor, (int) 1e6); - gyroscopeSensor.sensorManager.registerListener(this, gyroscopeSensor.sensor, 10000, (int) maxReportLatencyNs); - lightSensor.sensorManager.registerListener(this, lightSensor.sensor, (int) 1e6); - proximitySensor.sensorManager.registerListener(this, proximitySensor.sensor, (int) 1e6); - magnetometerSensor.sensorManager.registerListener(this, magnetometerSensor.sensor, 10000, (int) maxReportLatencyNs); - stepDetectionSensor.sensorManager.registerListener(this, stepDetectionSensor.sensor, SensorManager.SENSOR_DELAY_NORMAL); - rotationSensor.sensorManager.registerListener(this, rotationSensor.sensor, (int) 1e6); + if (accelerometerSensor.sensor != null) accelerometerSensor.sensorManager.registerListener(this, accelerometerSensor.sensor, 10000); + if (linearAccelerationSensor.sensor != null) linearAccelerationSensor.sensorManager.registerListener(this, linearAccelerationSensor.sensor, 10000); + if (gravitySensor.sensor != null) gravitySensor.sensorManager.registerListener(this, gravitySensor.sensor, 10000); + if (barometerSensor.sensor != null) barometerSensor.sensorManager.registerListener(this, barometerSensor.sensor, (int) 1e6); + if (gyroscopeSensor.sensor != null) gyroscopeSensor.sensorManager.registerListener(this, gyroscopeSensor.sensor, 10000); + if (lightSensor.sensor != null) lightSensor.sensorManager.registerListener(this, lightSensor.sensor, (int) 1e6); + if (proximitySensor.sensor != null) proximitySensor.sensorManager.registerListener(this, proximitySensor.sensor, (int) 1e6); + if (magnetometerSensor.sensor != null) magnetometerSensor.sensorManager.registerListener(this, magnetometerSensor.sensor, 10000); + if (stepDetectionSensor.sensor != null) stepDetectionSensor.sensorManager.registerListener(this, stepDetectionSensor.sensor, SensorManager.SENSOR_DELAY_NORMAL); + if (rotationSensor.sensor != null) rotationSensor.sensorManager.registerListener(this, rotationSensor.sensor, 10000); + wifiProcessor.startListening(); gnssProcessor.startLocationUpdates(); + startBleScan(); } - /** - * Un-registers all device listeners and pauses data collection. - * - * Should be called from {@link MainActivity} when pausing the application. - * - * @see MovementSensor handles SensorManager based devices. - * @see WifiDataProcessor handles wifi data. - * @see GNSSDataProcessor handles location data. - */ public void stopListening() { if(!saveRecording) { - // Unregister sensor-manager based devices accelerometerSensor.sensorManager.unregisterListener(this); barometerSensor.sensorManager.unregisterListener(this); gyroscopeSensor.sensorManager.unregisterListener(this); @@ -831,53 +1209,235 @@ public void stopListening() { rotationSensor.sensorManager.unregisterListener(this); linearAccelerationSensor.sensorManager.unregisterListener(this); gravitySensor.sensorManager.unregisterListener(this); - //The app often crashes here because the scan receiver stops after it has found the list. - // It will only unregister one if there is to unregister try { - this.wifiProcessor.stopListening(); //error here? + this.wifiProcessor.stopListening(); } catch (Exception e) { System.err.println("Wifi resumed before existing"); } - // Stop receiving location updates this.gnssProcessor.stopUpdating(); } } - /** - * Enables saving sensor values to the trajectory object. - * - * Sets save recording to true, resets the absolute start time and create new timer object for - * periodically writing data to trajectory. - * - * @see Traj object for storing data. - */ - public void startRecording() { - // If wakeLock is null (e.g. not initialized or was cleared), reinitialize it. + private void startBleScan() { + // Keep scanner singleton semantics to avoid duplicate callbacks and leaks. + if (bluetoothLeScanner != null && bleScanCallback != null) { + android.util.Log.d("SensorFusion", "BLE scan already running"); + return; + } + + if (bluetoothAdapter == null) { + android.util.Log.w("SensorFusion", "Bluetooth adapter is null"); + return; + } + + if (!bluetoothAdapter.isEnabled()) { + android.util.Log.w("SensorFusion", "Bluetooth is not enabled"); + return; + } + + bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner(); + if (bluetoothLeScanner == null) { + android.util.Log.w("SensorFusion", "Bluetooth LE scanner is null"); + return; + } + + bleScanCallback = new ScanCallback() { + @Override + public void onScanResult(int callbackType, ScanResult result) { + super.onScanResult(callbackType, result); + + // Maintain latest BLE device snapshot for UI rendering. + String macAddress = result.getDevice().getAddress(); + String name = null; + try { + if (ActivityCompat.checkSelfPermission(appContext, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) { + name = result.getDevice().getName(); + } + } catch (SecurityException ignored) { } + + int rssi = result.getRssi(); + + // Update RSSI for known devices, otherwise append a new device entry. + synchronized (bleList) { + boolean found = false; + for (BleDevice device : bleList) { + if (device.getMacAddress().equals(macAddress)) { + device.setRssi(rssi); + if (name != null) { + device.setName(name); + } + found = true; + break; + } + } + if (!found) { + bleList.add(new BleDevice(macAddress, name, rssi)); + android.util.Log.d("SensorFusion", "New BLE device added: " + macAddress); + } + } + + // Persist BLE observations in trajectory while recording. + if (saveRecording && trajectory != null) { + Traj.BleData.Builder bleBuilder = Traj.BleData.newBuilder() + .setMacAddress(macAddress) + .setTxPowerLevel(result.getTxPower()); + + if (name != null) { + bleBuilder.setName(name); + } + + trajectory.addBleData(bleBuilder.build()); + } + } + + @Override + public void onScanFailed(int errorCode) { + super.onScanFailed(errorCode); + android.util.Log.e("SensorFusion", "BLE scan failed with error code: " + errorCode); + } + }; + + try { + if (ActivityCompat.checkSelfPermission(appContext, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED) { + bluetoothLeScanner.startScan(bleScanCallback); + android.util.Log.d("SensorFusion", "BLE scan started successfully"); + } else { + android.util.Log.w("SensorFusion", "BLUETOOTH_SCAN permission not granted"); + } + } catch (SecurityException e) { + android.util.Log.e("SensorFusion", "SecurityException starting BLE scan: " + e.getMessage()); + } + } + + private void stopBleScan() { + if (bluetoothLeScanner != null && bleScanCallback != null) { + try { + if (ActivityCompat.checkSelfPermission(appContext, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED) { + bluetoothLeScanner.stopScan(bleScanCallback); + android.util.Log.d("SensorFusion", "BLE scan stopped"); + } + } catch (SecurityException e) { + android.util.Log.e("SensorFusion", "Error stopping BLE scan: " + e.getMessage()); + } + } + bleScanCallback = null; + bluetoothLeScanner = null; + } + + // Start recording with the specified trajectory ID. + public synchronized void startRecording(String trajectoryId) { + if (this.saveRecording) { + Log.w("SensorFusion", "startRecording called while already recording; ignoring duplicate start"); + return; + } + + if (this.storeTrajectoryTimer != null) { + try { + this.storeTrajectoryTimer.cancel(); + this.storeTrajectoryTimer.purge(); + } catch (Exception ignored) { + } + this.storeTrajectoryTimer = null; + } + if (wakeLock == null) { PowerManager powerManager = (PowerManager) this.appContext.getSystemService(Context.POWER_SERVICE); - wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp::MyWakelockTag"); + wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "PositionMe::WakeLock"); + } + wakeLock.acquire(31 * 60 * 1000L); + + // Ensure every recording session has a non-empty ID. + if (trajectoryId == null || trajectoryId.isEmpty()) { + trajectoryId = "UnknownTraj_" + System.currentTimeMillis(); } - wakeLock.acquire(31 * 60 * 1000L /*31 minutes*/); + + // Keep explicit session identifier in logs for upload/replay traceability. + Log.d("SensorFusion", "Start recording with ID: " + trajectoryId); this.saveRecording = true; this.stepCounter = 0; + this.gnssRecordCount = 0; + this.pdrRecordCount = 0; + this.markerCount = 0; + this.currentHeading = orientation[2]; + this.displayHeading = normalizeHeadingDegrees(this.currentHeading); + this.displayHeadingInitialized = true; + this.hasLastPdrForHeading = false; + this.lastGyroTimestampNs = 0L; + this.gyroHeadingInitialized = false; this.absoluteStartTime = System.currentTimeMillis(); this.bootTime = SystemClock.uptimeMillis(); - // Protobuf trajectory class for sending sensor data to restful API + + // Initialize trajectory header and sensor metadata. this.trajectory = Traj.Trajectory.newBuilder() .setAndroidVersion(Build.VERSION.RELEASE) .setStartTimestamp(absoluteStartTime) - .setAccelerometerInfo(createInfoBuilder(accelerometerSensor)) - .setGyroscopeInfo(createInfoBuilder(gyroscopeSensor)) - .setMagnetometerInfo(createInfoBuilder(magnetometerSensor)) - .setBarometerInfo(createInfoBuilder(barometerSensor)) - .setLightSensorInfo(createInfoBuilder(lightSensor)); - + .setTrajectoryId(trajectoryId) + .setInitialHeading(currentHeading) + .setAccelerometerInfo(createSensorInfo(accelerometerSensor)) + .setGyroscopeInfo(createSensorInfo(gyroscopeSensor)) + .setMagnetometerInfo(createSensorInfo(magnetometerSensor)) + .setBarometerInfo(createSensorInfo(barometerSensor)) + .setLightSensorInfo(createSensorInfo(lightSensor)); + + // Start origin policy: + // Use manual start when provided. + // Indoors, prefer WiFi start. + // Outdoors, use GNSS start. + boolean hasManualStart = startLocation != null && startLocation[0] != 0 && startLocation[1] != 0; + LatLng wifiInitialLocation = lastAcceptedWifiLocation != null ? lastAcceptedWifiLocation : getLatLngWifiPositioning(); + boolean hasWifiStart = wifiInitialLocation != null; + boolean indoorsByCurrentFix = isInsideKnownIndoorBuildings(latitude, longitude); + + float initLat; + float initLon; + boolean canInitializeNow; + if (hasManualStart) { + initLat = startLocation[0]; + initLon = startLocation[1]; + canInitializeNow = true; + Log.d("SensorFusion", "Using Manual Start Location: " + initLat + ", " + initLon); + } else if (indoorsByCurrentFix && hasWifiStart) { + initLat = (float) wifiInitialLocation.latitude; + initLon = (float) wifiInitialLocation.longitude; + canInitializeNow = true; + Log.d("SensorFusion", "Using WiFi Start Location: " + initLat + ", " + initLon); + } else if (!indoorsByCurrentFix) { + initLat = latitude; + initLon = longitude; + canInitializeNow = true; + Log.d("SensorFusion", "Using GNSS Start Location (outdoor): " + initLat + ", " + initLon); + } else { + // Indoors without a WiFi location: defer fusion initialization until callback arrives. + initLat = latitude; + initLon = longitude; + canInitializeNow = false; + Log.d("SensorFusion", "Indoor start detected but WiFi unavailable, waiting for WiFi initialization"); + } + Traj.GNSSPosition initialPos = Traj.GNSSPosition.newBuilder() + .setLatitude(initLat) + .setLongitude(initLon) + .setAltitude(altitude_val) + .setRelativeTimestamp(0) + .build(); + this.trajectory.setInitialPosition(initialPos); - this.storeTrajectoryTimer = new Timer(); + this.storeTrajectoryTimer = new Timer("trajectory-store-timer", true); this.storeTrajectoryTimer.schedule(new storeDataInTrajectory(), 0, TIME_CONST); this.pdrProcessing.resetPDR(); + if (canInitializeNow) { + this.pdrProcessing.initializeWithLocation(initLat, initLon); + setStartGNSSLatitude(new float[]{initLat, initLon}); + } + + // Reset step-detection windows for a clean recording session. + accWindowIndex = 0; + for(int i=0; i 0) { + Traj.GNSSReading firstGnss = sentTrajectory.getGnssData(0); + Traj.GNSSReading lastGnss = sentTrajectory.getGnssData(sentTrajectory.getGnssDataCount() - 1); + Log.d("SensorFusion", "First GNSS: lat=" + firstGnss.getPosition().getLatitude() + + ", lon=" + firstGnss.getPosition().getLongitude()); + Log.d("SensorFusion", "Last GNSS: lat=" + lastGnss.getPosition().getLatitude() + + ", lon=" + lastGnss.getPosition().getLongitude()); + } else { + Log.e("SensorFusion", "WARNING: No GNSS data in trajectory!"); + } + + if (sentTrajectory.getPdrDataCount() > 0) { + Traj.RelativePosition firstPdr = sentTrajectory.getPdrData(0); + Traj.RelativePosition lastPdr = sentTrajectory.getPdrData(sentTrajectory.getPdrDataCount() - 1); + Log.d("SensorFusion", "First PDR: x=" + firstPdr.getX() + ", y=" + firstPdr.getY()); + Log.d("SensorFusion", "Last PDR: x=" + lastPdr.getX() + ", y=" + lastPdr.getY()); + } else { + Log.e("SensorFusion", "WARNING: No PDR data in trajectory!"); + } + + Log.d("SensorFusion", "Campaign: " + (campaign.isEmpty() ? "(none)" : campaign)); + Log.d("SensorFusion", "=========================================="); + + this.serverCommunications.sendTrajectory(sentTrajectory, campaign); + } else { + Log.e("SensorFusion", "ERROR: trajectory is null, cannot send!"); + } } - /** - * Timer task to record data with the desired frequency in the trajectory class. - * - * Inherently threaded, runnables are created in {@link SensorFusion#startRecording()} and - * destroyed in {@link SensorFusion#stopRecording()}. - */ private class storeDataInTrajectory extends TimerTask { public void run() { - // Store IMU and magnetometer data in Trajectory class - trajectory.addImuData(Traj.Motion_Sample.newBuilder() - .setRelativeTimestamp(SystemClock.uptimeMillis()-bootTime) - .setAccX(acceleration[0]) - .setAccY(acceleration[1]) - .setAccZ(acceleration[2]) - .setGyrX(angularVelocity[0]) - .setGyrY(angularVelocity[1]) - .setGyrZ(angularVelocity[2]) - .setGyrZ(angularVelocity[2]) - .setRotationVectorX(rotation[0]) - .setRotationVectorY(rotation[1]) - .setRotationVectorZ(rotation[2]) - .setRotationVectorW(rotation[3]) - .setStepCount(stepCounter)) - .addPositionData(Traj.Position_Sample.newBuilder() - .setMagX(magneticField[0]) - .setMagY(magneticField[1]) - .setMagZ(magneticField[2]) - .setRelativeTimestamp(SystemClock.uptimeMillis()-bootTime)) -// .addGnssData(Traj.GNSS_Sample.newBuilder() -// .setLatitude(latitude) -// .setLongitude(longitude) -// .setRelativeTimestamp(SystemClock.uptimeMillis()-bootTime)) - ; - - // Divide timer with a counter for storing data every 1 second + if (trajectory == null) return; + long relTime = SystemClock.uptimeMillis() - bootTime; + + // High-rate IMU and orientation packet. + trajectory.addImuData(Traj.IMUReading.newBuilder() + .setRelativeTimestamp(relTime) + .setAcc(toVector3(acceleration)) + .setGyr(toVector3(angularVelocity)) + .setRotationVector(toQuaternion(rotation)) + .setStepCount(stepCounter) + .build()); + + // Magnetometer stream is stored independently for replay/analysis. + trajectory.addMagnetometerData(Traj.MagnetometerReading.newBuilder() + .setRelativeTimestamp(relTime) + .setMag(toVector3(magneticField)) + .build()); + if (counter == 99) { counter = 0; - // Store pressure and light data + + // Store slow-changing sensors at 1 Hz. if (barometerSensor.sensor != null) { - trajectory.addPressureData(Traj.Pressure_Sample.newBuilder() - .setPressure(pressure) - .setRelativeTimestamp(SystemClock.uptimeMillis() - bootTime)) - .addLightData(Traj.Light_Sample.newBuilder() - .setLight(light) - .setRelativeTimestamp(SystemClock.uptimeMillis() - bootTime) - .build()); + trajectory.addPressureData(Traj.BarometerReading.newBuilder() + .setPressure(pressure) + .setRelativeTimestamp(relTime) + .build()); + } + + if (lightSensor.sensor != null) { + trajectory.addLightData(Traj.LightReading.newBuilder() + .setLight(light) + .setRelativeTimestamp(relTime) + .build()); + } + + if(proximitySensor.sensor != null) { + trajectory.addProximityData(Traj.ProximityReading.newBuilder() + .setDistance(proximity) + .setRelativeTimestamp(relTime) + .build()); } - // Divide the timer for storing AP data every 5 seconds if (secondCounter == 4) { secondCounter = 0; - //Current Wifi Object + // Store representative AP metadata at 0.2 Hz. Wifi currentWifi = wifiProcessor.getCurrentWifiData(); - trajectory.addApsData(Traj.AP_Data.newBuilder() - .setMac(currentWifi.getBssid()) - .setSsid(currentWifi.getSsid()) - .setFrequency(currentWifi.getFrequency())); + if (currentWifi != null) { + trajectory.addApsData(Traj.WiFiAPData.newBuilder() + .setMac(currentWifi.getBssid()) + .setSsid(currentWifi.getSsid() != null ? currentWifi.getSsid() : "") + .setFrequency(currentWifi.getFrequency()) + .build()); + } } else { secondCounter++; @@ -1005,10 +1593,275 @@ public void run() { else { counter++; } + } + } + + // endregion + + public Map getSensorValueMap() { + Map sensorValueMap = new HashMap<>(); + + sensorValueMap.put(SensorTypes.ACCELEROMETER, acceleration); + sensorValueMap.put(SensorTypes.GRAVITY, gravity); + sensorValueMap.put(SensorTypes.MAGNETICFIELD, magneticField); + sensorValueMap.put(SensorTypes.GYRO, angularVelocity); + sensorValueMap.put(SensorTypes.LIGHT, new float[]{light}); + sensorValueMap.put(SensorTypes.PRESSURE, new float[]{pressure}); + sensorValueMap.put(SensorTypes.PROXIMITY, new float[]{proximity}); + sensorValueMap.put(SensorTypes.GNSSLATLONG, new float[]{latitude, longitude}); + sensorValueMap.put(SensorTypes.PDR, pdrProcessing.getPDRMovement()); + + // Fused position output used by map and trajectory overlays. + LatLng fusedLatLng = pdrProcessing.getFusedLatLon(); + if (fusedLatLng != null) { + sensorValueMap.put(SensorTypes.FUSED, new float[]{(float) fusedLatLng.latitude, (float) fusedLatLng.longitude}); + } else { + sensorValueMap.put(SensorTypes.FUSED, new float[]{0f, 0f}); + } + + // WiFi-only position output from network fingerprinting. + LatLng wifiLatLng = wiFiPositioning.getWifiLocation(); + if (wifiLatLng != null) { + sensorValueMap.put(SensorTypes.WIFI, new float[]{(float) wifiLatLng.latitude, (float) wifiLatLng.longitude}); + } else { + sensorValueMap.put(SensorTypes.WIFI, new float[]{0f, 0f}); + } + + sensorValueMap.put(SensorTypes.BLE, new float[]{0f}); + + return sensorValueMap; + } + + public List getWifiList() { + return wifiList; + } + + public List getBleList() { + synchronized (bleList) { + return new ArrayList<>(bleList); + } + } + + public float[] getGNSSLatitude(boolean start) { + float [] latLong = new float[2]; + if(!start) { + latLong[0] = latitude; + latLong[1] = longitude; + } + else{ + latLong = startLocation; + } + return latLong; + } + + public void setStartGNSSLatitude(float[] startPosition){ + startLocation = startPosition; + } + + public void redrawPath(float scalingRatio){ + pathView.redraw(scalingRatio); + } + + public float passAverageStepLength(){ + return pdrProcessing.getAverageStepLength(); + } + + public float passOrientation(){ + return orientation[0]; + } + + public List getSensorInfos() { + List infoList = new ArrayList<>(); + if (accelerometerSensor != null && accelerometerSensor.sensorInfo != null) infoList.add(accelerometerSensor.sensorInfo); + if (gyroscopeSensor != null && gyroscopeSensor.sensorInfo != null) infoList.add(gyroscopeSensor.sensorInfo); + if (magnetometerSensor != null && magnetometerSensor.sensorInfo != null) infoList.add(magnetometerSensor.sensorInfo); + if (barometerSensor != null && barometerSensor.sensorInfo != null) infoList.add(barometerSensor.sensorInfo); + if (lightSensor != null && lightSensor.sensorInfo != null) infoList.add(lightSensor.sensorInfo); + if (proximitySensor != null && proximitySensor.sensorInfo != null) infoList.add(proximitySensor.sensorInfo); + if (stepDetectionSensor != null && stepDetectionSensor.sensorInfo != null) infoList.add(stepDetectionSensor.sensorInfo); + if (rotationSensor != null && rotationSensor.sensorInfo != null) infoList.add(rotationSensor.sensorInfo); + if (gravitySensor != null && gravitySensor.sensorInfo != null) infoList.add(gravitySensor.sensorInfo); + if (linearAccelerationSensor != null && linearAccelerationSensor.sensorInfo != null) infoList.add(linearAccelerationSensor.sensorInfo); + + // Append BLE scanner capability as a pseudo-sensor entry. + if (bluetoothAdapter != null) { + String adapterName = "System"; + try { + if (ActivityCompat.checkSelfPermission(appContext, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) { + String name = bluetoothAdapter.getName(); + if (name != null && !name.isEmpty()) { + adapterName = name; + } + } + } catch (SecurityException e) { + android.util.Log.d("SensorFusion", "Cannot get Bluetooth adapter name: " + e.getMessage()); + } + SensorInfo bleInfo = new SensorInfo( + "Bluetooth LE Scanner", + adapterName, + -1.0f, + 0.0f, + 0, + -1 + ); + infoList.add(bleInfo); } + + return infoList; } - //endregion + public void registerForServerUpdate(Observer observer) { + serverCommunications.registerObserver(observer); + } + + public float getElevation() { return this.elevation; } + public float getEstimatedAbsoluteAltitude() { return this.estimatedAbsoluteAltitude; } + public boolean getElevator() { return this.elevator; } + public float getGnssAccuracy() { return this.gnssAccuracy; } + public float getGnssVerticalAccuracy() { return this.gnssVerticalAccuracy; } + public LatLng getFusedLatLng() { + return pdrProcessing != null ? pdrProcessing.getFusedLatLon() : null; + } + public int getEstimatedFloor() { + if (!barometerAutoFloorEnabled) { + return pdrProcessing != null ? pdrProcessing.getCurrentFloor() : 0; + } + return getEstimatedFloorByBarometerBands(); + } + + public int getEstimatedFloorByBarometerBands() { + if (!barometerAutoFloorEnabled) { + return pdrProcessing != null ? pdrProcessing.getCurrentFloor() : 0; + } + + float altitudeForFloor = !Float.isNaN(estimatedAbsoluteAltitude) + ? estimatedAbsoluteAltitude + : (!Float.isNaN(smoothedBarometerAbsoluteAltitude) + ? smoothedBarometerAbsoluteAltitude + : barometerAbsoluteAltitude); + + if (Float.isNaN(altitudeForFloor)) { + return pdrProcessing != null ? pdrProcessing.getCurrentFloor() : 0; + } + + int rawFloor = getRawEstimatedFloorFromBands(altitudeForFloor); + if (lastStableEstimatedFloor == Integer.MIN_VALUE) { + lastStableEstimatedFloor = rawFloor; + return lastStableEstimatedFloor; + } + + int stableFloor = lastStableEstimatedFloor; + switch (stableFloor) { + case 0: + if (altitudeForFloor >= FLOOR_BAND_B1_MAX + FLOOR_BAND_HYSTERESIS_METERS) { + stableFloor = 1; + } + break; + case 1: + if (altitudeForFloor <= FLOOR_BAND_B1_MAX - FLOOR_BAND_HYSTERESIS_METERS) { + stableFloor = 0; + } else if (altitudeForFloor >= FLOOR_BAND_GF_MAX + FLOOR_BAND_HYSTERESIS_METERS) { + stableFloor = 2; + } + break; + case 2: + if (altitudeForFloor <= FLOOR_BAND_GF_MAX - FLOOR_BAND_HYSTERESIS_METERS) { + stableFloor = 1; + } else if (altitudeForFloor >= FLOOR_BAND_F1_MAX + FLOOR_BAND_HYSTERESIS_METERS) { + stableFloor = 3; + } + break; + case 3: + if (altitudeForFloor <= FLOOR_BAND_F1_MAX - FLOOR_BAND_HYSTERESIS_METERS) { + stableFloor = 2; + } else if (altitudeForFloor >= FLOOR_BAND_F2_MAX + FLOOR_BAND_HYSTERESIS_METERS) { + stableFloor = 4; + } + break; + default: + if (altitudeForFloor <= FLOOR_BAND_F2_MAX - FLOOR_BAND_HYSTERESIS_METERS) { + stableFloor = 3; + } + break; + } + + if (Math.abs(rawFloor - stableFloor) >= 2) { + stableFloor = rawFloor; + } + lastStableEstimatedFloor = stableFloor; + return lastStableEstimatedFloor; + } + + public void setBarometerAutoFloorEnabled(boolean enabled) { + if (this.barometerAutoFloorEnabled == enabled) { + return; + } + this.barometerAutoFloorEnabled = enabled; + this.lastStableEstimatedFloor = Integer.MIN_VALUE; + } + + public boolean isBarometerAutoFloorEnabled() { + return this.barometerAutoFloorEnabled; + } + + private int getRawEstimatedFloorFromBands(float altitudeForFloor) { + if (altitudeForFloor <= FLOOR_BAND_B1_MAX) { + return 0; // B1 + } + if (altitudeForFloor < FLOOR_BAND_GF_MAX) { + return 1; // GF + } + if (altitudeForFloor < FLOOR_BAND_F1_MAX) { + return 2; // F1 + } + if (altitudeForFloor < FLOOR_BAND_F2_MAX) { + return 3; // F2 + } + return 4; // F3+ + } + public void setIndoorFloorReference(float floorHeightMeters, int floorIndex, float[] floorAltitudeAnchorsMeters) { + if (pdrProcessing != null) { + pdrProcessing.configureFloorReference(floorHeightMeters, floorIndex, floorAltitudeAnchorsMeters); + } + } + + public void setIndoorEnvironmentFeatures(List> stairsZones, List> liftZones, List> walls) { + if (pdrProcessing != null) { + pdrProcessing.setIndoorFeatureZones(stairsZones, liftZones, walls); + } + } + + public int getHoldMode(){ + int proximityThreshold = 1, lightThreshold = 100; + if(proximitylightThreshold) return 1; + else return 0; + } + // endregion + + @Override + public void onAccuracyChanged(Sensor sensor, int i) {} + + public Traj.Trajectory.Builder getTrajectory() { + return this.trajectory; + } + public void resetPDR() { + if (pdrProcessing != null) { + pdrProcessing.refreshSettings(); + } + lastWifiCandidateLocation = null; + lastAcceptedWifiLocation = null; + stableWifiCandidateCount = 0; + } + + public void setMagneticCompensationEnabled(boolean enabled) { + if (pdrProcessing != null) { + pdrProcessing.setMagneticCompensationEnabled(enabled); + } + } + + public boolean isMagneticCompensationEnabled() { + return pdrProcessing != null && pdrProcessing.isMagneticCompensationEnabled(); + } } diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorInfo.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorInfo.java index 23b92848..11df589b 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorInfo.java +++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorInfo.java @@ -1,15 +1,9 @@ package com.openpositioning.PositionMe.sensors; -import androidx.annotation.NonNull; - -/** - * The Sensor Info object holds physical properties of a sensor in the device. - * - * It contains it's name, vendor, resolution, power, version and type, which vary across devices. - * - * @author Virginia Cangelosi - * @author Mate Stodulka - */ +// The Sensor Info object holds physical properties of a sensor in the device. +// It contains it's name, vendor, resolution, power, version and type, which vary across devices. +// @author Virginia Cangelosi +// @author Mate Stodulka public class SensorInfo { private final String name; private final String vendor; @@ -18,18 +12,14 @@ public class SensorInfo { private final int version; private final int type; - /** - * Public default constructor of the Sensor Info object. - * - * Should be initialised with all its parameters, typically from {@link MovementSensor}. - * - * @param name name string of the sensor. Unique for a particular sensor type. - * @param vendor vendor string of this sensor. - * @param resolution resolution of the sensor in the sensor's unit. - * @param power the power in mA used by this sensor while in use. - * @param version version of the sensor's module. - * @param type generic type of this sensor. - */ + // Public default constructor of the Sensor Info object. + // Should be initialised with all its parameters, typically from {@link MovementSensor}. +// Parameter name: name string of the sensor. Unique for a particular sensor type. +// Parameter vendor: vendor string of this sensor. +// Parameter resolution: resolution of the sensor in the sensor's unit. +// Parameter power: the power in mA used by this sensor while in use. +// Parameter version: version of the sensor's module. +// Parameter type: generic type of this sensor. public SensorInfo(String name, String vendor, float resolution, float power, int version, int type) { this.name = name; this.vendor = vendor; @@ -40,7 +30,7 @@ public SensorInfo(String name, String vendor, float resolution, float power, int } - //region Getters + // region Getters public String getName() { return name; @@ -66,13 +56,10 @@ public int getType() { return type; } - //endregion + // endregion - /** - * {@inheritDoc} - * Basic string representation for debug - */ - @NonNull + // {@inheritDoc} + // Basic string representation for debug @Override public String toString() { return "SensorInfo{" + @@ -85,3 +72,5 @@ public String toString() { '}'; } } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorTypes.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorTypes.java index ee3bbcc1..122bc771 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorTypes.java +++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorTypes.java @@ -2,15 +2,11 @@ import com.openpositioning.PositionMe.presentation.fragment.MeasurementsFragment; -/** - * Enum of the sensor types. - * - * Simplified version of default Android Sensor.TYPE, with the order matching the table layout for - * the {@link MeasurementsFragment}. Includes virtual sensors and other - * data providing devices as well as derived data. - * - * @author Mate Stodulka - */ +// Enum of the sensor types. +// Simplified version of default Android Sensor.TYPE, with the order matching the table layout for +// the {@link MeasurementsFragment}. Includes virtual sensors and other +// data providing devices as well as derived data. +// @author Mate Stodulka public enum SensorTypes { ACCELEROMETER, GRAVITY, @@ -20,5 +16,9 @@ public enum SensorTypes { PRESSURE, PROXIMITY, GNSSLATLONG, - PDR; + PDR, + FUSED, + WIFI, + BLE; } + diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/WiFiPositioning.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/WiFiPositioning.java index dbf809dd..d5e2684f 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/sensors/WiFiPositioning.java +++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/WiFiPositioning.java @@ -10,43 +10,40 @@ import org.json.JSONException; import org.json.JSONObject; -/** - * Class for creating and handling POST requests for obtaining the current position using - * WiFi positioning API from https://openpositioning.org/api/position/fine - * - * The class creates POST requests based on WiFi fingerprints and obtains the user's location - * - * The request are handled asynchronously, The WiFi position coordinates and floor are updated - * when the response of the POST request is obtained. - * - * One can create a POST request using the function provided in the class (createPostRequest()) with - * the WiFi fingerprint - * Its then added to the RequestQueue to be handled asynchronously (not blocking the main thread) - * When the response to the request is obtained the wifiLocation and floor are updated. - * Calling the getters for wifiLocation and the floor allows obtaining the WiFi location and floor - * from the POST request response. - * @author Arun Gopalakrishnan - */ + +import java.util.HashMap; +import java.util.Map; +// Class for creating and handling POST requests for obtaining the current position using +// WiFi positioning API from https://openpositioning.org/api/position/fine +// The class creates POST requests based on WiFi fingerprints and obtains the user's location +// The request are handled asynchronously, The WiFi position coordinates and floor are updated +// when the response of the POST request is obtained. +// One can create a POST request using the function provided in the class (createPostRequest()) with +// the WiFi fingerprint +// Its then added to the RequestQueue to be handled asynchronously (not blocking the main thread) +// When the response to the request is obtained the wifiLocation and floor are updated. +// Calling the getters for wifiLocation and the floor allows obtaining the WiFi location and floor +// from the POST request response. +// @author Arun Gopalakrishnan public class WiFiPositioning { // Queue for storing the POST requests made private RequestQueue requestQueue; // URL for WiFi positioning API private static final String url="https://openpositioning.org/api/position/fine"; + // Optional API auth token (supports app-level auth over openpositioning API) + private String apiAuthToken = null; + private static final String FALLBACK_API_AUTH_TOKEN = "NO_AUTH"; // Demo value; can be loaded from secrets.properties. - /** - * Getter for the WiFi positioning coordinates obtained using openpositioning API - * @return the user's coordinates based on openpositioning API - */ + // Getter for the WiFi positioning coordinates obtained using openpositioning API +// Returns: the user's coordinates based on openpositioning API public LatLng getWifiLocation() { return wifiLocation; } // Store user's location obtained using WiFi positioning private LatLng wifiLocation; - /** - * Getter for the WiFi positioning floor obtained using openpositioning API - * @return the user's location based on openpositioning API - */ + // Getter for the WiFi positioning floor obtained using openpositioning API +// Returns: the user's location based on openpositioning API public int getFloor() { return floor; } @@ -55,127 +52,108 @@ public int getFloor() { private int floor=0; - /** - * Constructor to create the WiFi positioning object - * - * Initialising a request queue to handle the POST requests asynchronously - * - * @param context Context of object calling - */ + // Constructor to create the WiFi positioning object + // Initialising a request queue to handle the POST requests asynchronously +// Parameter context: Context of object calling public WiFiPositioning(Context context){ // Initialising the Request queue this.requestQueue = Volley.newRequestQueue(context.getApplicationContext()); } - /** - * Creates a POST request using the WiFi fingerprint to obtain user's location - * The POST request is issued to https://openpositioning.org/api/position/fine - * (the openpositioning API) with the WiFI fingerprint passed as the parameter. - * - * The response of the post request returns the coordinates of the WiFi position - * along with the floor of the building the user is at. - * - * A try and catch block along with error Logs have been added to keep a record of error's - * obtained while handling POST requests (for better maintainability and secure programming) - * - * @param jsonWifiFeatures WiFi Fingerprint from device - */ - public void request(JSONObject jsonWifiFeatures) { - // Creating the POST request using WiFi fingerprint (a JSON object) - JsonObjectRequest jsonObjectRequest = new JsonObjectRequest( - Request.Method.POST, url, jsonWifiFeatures, - // Parses the response to obtain the WiFi location and WiFi floor - response -> { - try { - wifiLocation = new LatLng(response.getDouble("lat"),response.getDouble("lon")); - floor = response.getInt("floor"); - } catch (JSONException e) { - // Error log to keep record of errors (for secure programming and maintainability) - Log.e("jsonErrors","Error parsing response: "+e.getMessage()+" "+ response); - } - }, - // Handles the errors obtained from the POST request - error -> { - // Validation Error - if (error.networkResponse!=null && error.networkResponse.statusCode==422){ - Log.e("WiFiPositioning", "Validation Error "+ error.getMessage()); - } - // Other Errors - else{ - // When Response code is available - if (error.networkResponse!=null) { - Log.e("WiFiPositioning","Response Code: " + error.networkResponse.statusCode + ", " + error.getMessage()); - } - else{ - Log.e("WiFiPositioning","Error message: " + error.getMessage()); - } - } - } - ); - // Adds the request to the request queue - requestQueue.add(jsonObjectRequest); + // Creates a POST request using the WiFi fingerprint to obtain user's location + // The POST request is issued to https://openpositioning.org/api/position/fine + // (the openpositioning API) with the WiFI fingerprint passed as the parameter. + // The response of the post request returns the coordinates of the WiFi position + // along with the floor of the building the user is at. + // A try and catch block along with error Logs have been added to keep a record of error's + // obtained while handling POST requests (for better maintainability and secure programming) +// Parameter jsonWifiFeatures: WiFi Fingerprint from device + public void setApiAuthToken(String token) { + if (token != null && !token.trim().isEmpty()) { + this.apiAuthToken = token; + } } + // Returns a valid token if provided or fallback + private String getEffectiveApiAuthToken() { + return (apiAuthToken != null && !apiAuthToken.isEmpty()) ? apiAuthToken : FALLBACK_API_AUTH_TOKEN; + } - /** - * Creates a POST request using the WiFi fingerprint to obtain user's location - * The POST request is issued to https://openpositioning.org/api/position/fine - * (the openpositioning API) with the WiFI fingerprint passed as the parameter. - * - * The response of the post request returns the coordinates of the WiFi position - * along with the floor of the building the user is at though a callback. - * - * A try and catch block along with error Logs have been added to keep a record of error's - * obtained while handling POST requests (for better maintainability and secure programming) - * - * @param jsonWifiFeatures WiFi Fingerprint from device - * @param callback callback function to allow user to use location when ready - */ - public void request( JSONObject jsonWifiFeatures, final VolleyCallback callback) { - // Creating the POST request using WiFi fingerprint (a JSON object) + // Creates a POST request using the WiFi fingerprint to obtain user's location + public void request(JSONObject jsonWifiFeatures) { + executeRequest(jsonWifiFeatures, null); + } + + private void executeRequest(JSONObject jsonWifiFeatures, VolleyCallback callback) { JsonObjectRequest jsonObjectRequest = new JsonObjectRequest( Request.Method.POST, url, jsonWifiFeatures, response -> { try { - Log.d("jsonObject",response.toString()); - wifiLocation = new LatLng(response.getDouble("lat"),response.getDouble("lon")); + wifiLocation = new LatLng(response.getDouble("lat"), response.getDouble("lon")); floor = response.getInt("floor"); - callback.onSuccess(wifiLocation,floor); + if (callback != null) callback.onSuccess(wifiLocation, floor); } catch (JSONException e) { - Log.e("jsonErrors","Error parsing response: "+e.getMessage()+" "+ response); - callback.onError("Error parsing response: " + e.getMessage()); + Log.e("WiFiPositioning", "Parse error: " + e.getMessage() + " -- " + response); + if (callback != null) callback.onError("Error parsing response: " + e.getMessage()); } }, error -> { - // Validation Error - if (error.networkResponse!=null && error.networkResponse.statusCode==422){ - Log.e("WiFiPositioning", "Validation Error "+ error.getMessage()); - callback.onError( "Validation Error (422): "+ error.getMessage()); - } - // Other Errors - else{ - // When Response code is available - if (error.networkResponse!=null) { - Log.e("WiFiPositioning","Response Code: " + error.networkResponse.statusCode + ", " + error.getMessage()); - callback.onError("Response Code: " + error.networkResponse.statusCode + ", " + error.getMessage()); - } - else{ - Log.e("WiFiPositioning","Error message: " + error.getMessage()); - callback.onError("Error message: " + error.getMessage()); + if (error.networkResponse != null && error.networkResponse.statusCode == 422) { + Log.e("WiFiPositioning", "Validation Error " + error.getMessage()); + if (callback != null) callback.onError("Validation Error (422): " + error.getMessage()); + } else if (error.networkResponse != null && error.networkResponse.statusCode == 401) { + Log.e("WiFiPositioning", "Unauthorized - API auth required"); + if (callback != null) callback.onError("Unauthorized (401) - check API token"); + } else { + if (error.networkResponse != null) { + Log.e("WiFiPositioning", "Response Code: " + error.networkResponse.statusCode + ", " + error.getMessage()); + if (callback != null) callback.onError("Response Code: " + error.networkResponse.statusCode + ", " + error.getMessage()); + } else { + Log.e("WiFiPositioning", "Error message: " + error.getMessage()); + if (callback != null) callback.onError("Error message: " + error.getMessage()); } } + }) { + @Override + public Map getHeaders() { + Map headers = new HashMap<>(); + headers.put("Content-Type", "application/json"); + if (getEffectiveApiAuthToken() != null && !FALLBACK_API_AUTH_TOKEN.equals(getEffectiveApiAuthToken())) { + headers.put("Authorization", "Bearer " + getEffectiveApiAuthToken()); } - ); - // Adds the request to the request queue + return headers; + } + }; + + jsonObjectRequest.setRetryPolicy(new com.android.volley.DefaultRetryPolicy( + 6000, + 2, + com.android.volley.DefaultRetryPolicy.DEFAULT_BACKOFF_MULT + )); + requestQueue.add(jsonObjectRequest); } - /** - * Interface defined for the callback to access response obtained after POST request - */ + + + // Creates a POST request using the WiFi fingerprint to obtain user's location + // The POST request is issued to https://openpositioning.org/api/position/fine + // (the openpositioning API) with the WiFI fingerprint passed as the parameter. + // The response of the post request returns the coordinates of the WiFi position + // along with the floor of the building the user is at though a callback. + // A try and catch block along with error Logs have been added to keep a record of error's + // obtained while handling POST requests (for better maintainability and secure programming) +// Parameter jsonWifiFeatures: WiFi Fingerprint from device +// Parameter callback: callback function to allow user to use location when ready + public void request(JSONObject jsonWifiFeatures, final VolleyCallback callback) { + executeRequest(jsonWifiFeatures, callback); + } + + // Interface defined for the callback to access response obtained after POST request public interface VolleyCallback { void onSuccess(LatLng location, int floor); void onError(String message); } -} \ No newline at end of file +} + diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/Wifi.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/Wifi.java index d2e981cb..c7f05cd4 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/sensors/Wifi.java +++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/Wifi.java @@ -2,51 +2,44 @@ import com.openpositioning.PositionMe.presentation.fragment.MeasurementsFragment; -/** - * The Wifi object holds the Wifi parameters listed below. - * - * It contains the ssid (the identifier of the wifi), bssid (the mac address of the wifi), level - * (the strength of the wifi in dB) and frequency (the frequency of the wifi network (2.4GHz or - * 5GHz). For most objects only the bssid and the level are set. - * - * @author Virginia Cangelosi - * @author Mate Stodulka - */ +// The Wifi object holds the Wifi parameters listed below. +// It contains the ssid (the identifier of the wifi), bssid (the mac address of the wifi), level +// (the strength of the wifi in dB) and frequency (the frequency of the wifi network (2.4GHz or +// 5GHz). For most objects only the bssid and the level are set. +// @author Virginia Cangelosi +// @author Mate Stodulka public class Wifi { private String ssid; private long bssid; private int level; private long frequency; + private Float rttAltitudeMeters; - /** - * Empty public default constructor of the Wifi object. - */ + // Empty public default constructor of the Wifi object. public Wifi(){} - /** - * Getters for each property - */ + // Getters for each property public String getSsid() { return ssid; } public long getBssid() { return bssid; } public int getLevel() { return level; } public long getFrequency() { return frequency; } + public Float getRttAltitudeMeters() { return rttAltitudeMeters; } + public boolean hasRttAltitude() { return rttAltitudeMeters != null; } - /** - * Setters for each property - */ + // Setters for each property public void setSsid(String ssid) { this.ssid = ssid; } public void setBssid(long bssid) { this.bssid = bssid; } public void setLevel(int level) { this.level = level; } public void setFrequency(long frequency) { this.frequency = frequency; } + public void setRttAltitudeMeters(Float rttAltitudeMeters) { this.rttAltitudeMeters = rttAltitudeMeters; } - /** - * Generates a string containing mac address and rssi of Wifi. - * - * Concatenates mac address and rssi to display in the - * {@link MeasurementsFragment} fragment - */ + // Generates a string containing mac address and rssi of Wifi. + // Concatenates mac address and rssi to display in the + // {@link MeasurementsFragment} fragment @Override public String toString() { return "bssid: " + bssid +", level: " + level; } } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/WifiDataProcessor.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/WifiDataProcessor.java index fa8a17dd..897814e1 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/sensors/WifiDataProcessor.java +++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/WifiDataProcessor.java @@ -6,186 +6,172 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; +import android.net.MacAddress; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.wifi.ScanResult; import android.net.wifi.WifiManager; +import android.net.wifi.rtt.RangingRequest; +import android.net.wifi.rtt.RangingResult; +import android.net.wifi.rtt.RangingResultCallback; +import android.net.wifi.rtt.ResponderLocation; +import android.net.wifi.rtt.WifiRttManager; +import android.os.Build; import android.provider.Settings; +import android.util.Log; import android.widget.Toast; import androidx.core.app.ActivityCompat; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.Timer; import java.util.TimerTask; -/** - * The WifiDataProcessor class is the Wi-Fi data gathering and processing class of the application. - * It implements the wifi scanning and broadcasting design to identify a list of nearby Wi-Fis as - * well as collecting information about the current Wi-Fi connection. - *

- * The class implements {@link Observable} for informing {@link Observer} classes of updated - * variables. As such, it implements the {@link WifiDataProcessor#notifyObservers(int idx)} function and - * the {@link WifiDataProcessor#registerObserver(Observer o)} function to add new users which will - * be notified of new changes. - *

- * The class ensures all required permissions are granted before enabling the Wi-Fi. The class will - * periodically start a wifi scan as determined by {@link SensorFusion}. When a broadcast is - * received it will collect a list of users and notify users. The - * {@link WifiDataProcessor#getCurrentWifiData()} function will return information about the current - * Wi-Fi when called by {@link SensorFusion}. - * - * @author Mate Stodulka - * @author Virginia Cangelosi - */ + +// Collects periodic WiFi scans and publishes normalized results to observers. public class WifiDataProcessor implements Observable { - //Time over which a new scan will be initiated - private static final long scanInterval = 5000; + // Time over which a new scan will be initiated. + // Reduced to 1 second for faster indoor WiFi positioning updates. + private static final long scanInterval = 10000; // Application context for handling permissions and WifiManager instances private final Context context; - // Locations manager to enable access to Wifi data via the android system + // Wifi manager to enable access to Wifi data via the android system private final WifiManager wifiManager; + private final WifiRttManager wifiRttManager; - //List of nearby networks + // List of nearby networks private Wifi[] wifiData; - //List of observers to be notified when changes are detected + // List of observers to be notified when changes are detected private ArrayList observers; // Timer object private Timer scanWifiDataTimer; - /** - * Public default constructor of the WifiDataProcessor class. - * The constructor saves the context, checks for permissions to use the location services, - * creates an instance of the shared preferences to access settings using the context, - * initialises the wifi manager, and creates a timer object and list of observers. It checks if - * wifi is enabled and enables wifi scans every 5seconds. It also informs the user to disable - * wifi throttling if the device implements it. - * - * @param context Application Context to be used for permissions and device accesses. - * - * @see SensorFusion the intended parent class. - * - * @author Virginia Cangelosi - * @author Mate Stodulka - */ + // Tracks registration state to avoid double-unregister exceptions. + private boolean isReceiverRegistered = false; + private boolean isRttRanging = false; + + // Public default constructor of the WifiDataProcessor class. public WifiDataProcessor(Context context) { this.context = context; // Check for permissions boolean permissionsGranted = checkWifiPermissions(); this.wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + this.wifiRttManager = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P + ? (WifiRttManager) context.getSystemService(Context.WIFI_RTT_RANGING_SERVICE) + : null; this.scanWifiDataTimer = new Timer(); this.observers = new ArrayList<>(); - // Decreapted method after API 29 - // Turn on wifi if it is currently disabled - // TODO - turn it to a notification toward user -// // if(permissionsGranted && wifiManager.getWifiState()== WifiManager.WIFI_STATE_DISABLED) { -// // wifiManager.setWifiEnabled(true); -// // } - // Start wifi scan and return results via broadcast if(permissionsGranted) { + // Schedule the first scan immediately (0 delay) this.scanWifiDataTimer.schedule(new scheduledWifiScan(), 0, scanInterval); } - //Inform the user if wifi throttling is enabled on their device + // Inform the user if wifi throttling is enabled on their device checkWifiThrottling(); } - /** - * Broadcast receiver to receive updates from the wifi manager. - * Receives updates when a wifi scan is complete. Observers are notified when the broadcast is - * received to update the list of wifis - */ + // Broadcast receiver to receive updates from the wifi manager. BroadcastReceiver wifiScanReceiver = new BroadcastReceiver() { - /** - * Updates the list of nearby wifis when the broadcast is received. - * Ensures wifi scans are not enabled if permissions are not granted. The list of wifis is - * then passed to store the Mac Address and strength and observers of the WifiDataProcessor - * class are notified of the updated wifi list. - * - * - * @param context Application Context to be used for permissions and device accesses. - * @param intent ???. - */ @Override public void onReceive(Context context, Intent intent) { - + // Safety check for permissions if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { - // Unregister this listener - stopListening(); return; } - //Collect the list of nearby wifis - List wifiScanList = wifiManager.getScanResults(); - //Stop receiver as scan is complete - context.unregisterReceiver(this); - - //Loop though each item in wifi list - wifiData = new Wifi[wifiScanList.size()]; - for(int i = 0; i < wifiScanList.size(); i++) { - wifiData[i] = new Wifi(); - //Convert String mac address to an integer - String wifiMacAddress = wifiScanList.get(i).BSSID; - long intMacAddress = convertBssidToLong(wifiMacAddress); - //store mac address and rssi of wifi - wifiData[i].setBssid(intMacAddress); - wifiData[i].setLevel(wifiScanList.get(i).level); + // Unregister receiver immediately to prevent leaks, but check flag first + // Note: In this design, we register for EACH scan and unregister immediately after. + try { + if (isReceiverRegistered) { + context.unregisterReceiver(this); + isReceiverRegistered = false; + } + } catch (IllegalArgumentException e) { + // Ignore if already unregistered } - //Notify observers of change in wifiData variable - notifyObservers(0); + // Check for success + boolean success = intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false); + if (success) { + processScanResults(); + } else { + // Some devices report scan failure under throttling but still keep recent cached results. + Log.w("WifiDataProcessor", "Scan failure received. Falling back to cached scan results."); + processScanResults(); + } } }; - /** - * Converts mac address from string to integer. - * Removes semicolons from mac address and converts each hex byte to a hex integer. - * - * - * @param wifiMacAddress String Mac Address received from WifiManager containing colons - * - * @return Long variable with decimal conversion of the mac address - */ - private long convertBssidToLong(String wifiMacAddress){ - long intMacAddress =0; - int colonCount =5; - //Loop through each character - for(int j =0; j<17; j++){ - //Identify character - char macByte = wifiMacAddress.charAt(j); - //convert string hex mac address with colons to decimal long integer - if(macByte != ':'){ - //For characters 0-9 subtract 48 from ASCII code and multiply by 16^position - if((int) macByte >= 48 && (int) macByte <= 57){ - intMacAddress = intMacAddress + (((int)macByte-48)*((long)Math.pow(16,16-j-colonCount))); - } + // Process the successful scan results. + private void processScanResults() { + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + return; + } - //For characters a-f subtract 87 (=97-10) from ASCII code and multiply by 16^index - else if ((int) macByte >= 97 && (int) macByte <= 102){ - intMacAddress = intMacAddress + (((int)macByte-87)*((long)Math.pow(16,16-j-colonCount))); - } + List wifiScanList = wifiManager.getScanResults(); + + // Use a set to filter duplicates based on BSSID + List uniqueWifiList = new ArrayList<>(); + Set seenBssids = new HashSet<>(); + + for (ScanResult result : wifiScanList) { + // Skip if we've already seen this MAC in this batch + if (seenBssids.contains(result.BSSID)) { + continue; } - else - //coloncount is used to obtain the index of each character - colonCount --; + seenBssids.add(result.BSSID); + + Wifi wifi = new Wifi(); + + // MAC / BSSID + long intMacAddress = convertBssidToLong(result.BSSID); + wifi.setBssid(intMacAddress); + + // RSSI / Level + wifi.setLevel(result.level); + + + wifi.setSsid(result.SSID != null ? result.SSID : ""); + + + wifi.setFrequency(result.frequency); + + uniqueWifiList.add(wifi); } - return intMacAddress; + // Convert List to Array for compatibility with existing Observer interface + wifiData = uniqueWifiList.toArray(new Wifi[0]); + + // Notify observers of change + notifyObservers(0); + + startRttAltitudeRanging(wifiScanList); + } + + // Converts mac address from string to integer (Robust version). + private long convertBssidToLong(String wifiMacAddress){ + if (wifiMacAddress == null || wifiMacAddress.isEmpty()) return 0; + try { + // Remove colons and parse as hex + String hex = wifiMacAddress.replace(":", "").replace("-", "").trim(); + // Use Long.parseUnsignedLong to handle large MAC values correctly + return Long.parseUnsignedLong(hex, 16); + } catch (NumberFormatException e) { + // Fallback or log error + Log.e("WifiDataProcessor", "Error parsing MAC: " + wifiMacAddress); + return 0; + } } - /** - * Checks if the user authorised all permissions necessary for accessing wifi data. - * Explicit user permissions must be granted for android sdk version 23 and above. This - * function checks which permissions are granted, and returns their conjunction. - * - * @return boolean true if all permissions are granted for wifi access, false otherwise. - */ + // Checks if the user authorised all permissions necessary for accessing wifi data. private boolean checkWifiPermissions() { int wifiAccessPermission = ActivityCompat.checkSelfPermission(this.context, Manifest.permission.ACCESS_WIFI_STATE); @@ -196,133 +182,228 @@ private boolean checkWifiPermissions() { int fineLocationPermission = ActivityCompat.checkSelfPermission(this.context, Manifest.permission.ACCESS_FINE_LOCATION); - // Return missing permissions return wifiAccessPermission == PackageManager.PERMISSION_GRANTED && wifiChangePermission == PackageManager.PERMISSION_GRANTED && coarseLocationPermission == PackageManager.PERMISSION_GRANTED && fineLocationPermission == PackageManager.PERMISSION_GRANTED; } - /** - * Scan for nearby networks. - * The method checks for permissions again, and then requests a scan of nearby wifis. A - * broadcast receiver is registered to be called when the scan is complete. - */ + // Scan for nearby networks. private void startWifiScan() { - //Check settings for wifi permissions if(checkWifiPermissions()) { - //if(sharedPreferences.getBoolean("wifi", false)) { - //Register broadcast receiver for wifi scans - context.registerReceiver(wifiScanReceiver, new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)); - wifiManager.startScan(); + try { + // Register receiver ONLY if not already registered + if (!isReceiverRegistered) { + context.registerReceiver(wifiScanReceiver, new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)); + isReceiverRegistered = true; + } + boolean started = wifiManager.startScan(); + if (!started) { + // If startScan is throttled, use last available scan cache to keep pipeline alive. + Log.d("WifiDataProcessor", "Wifi Scan start failed (likely throttled), using cached results"); + processScanResults(); + } + } catch (Exception e) { + Log.e("WifiDataProcessor", "Error starting scan: " + e.getMessage()); + isReceiverRegistered = false; // Reset flag on error + } + } + } + + private void startRttAltitudeRanging(List wifiScanList) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P || wifiRttManager == null || isRttRanging) { + return; + } + + if (!wifiRttManager.isAvailable()) { + return; + } + + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + return; + } + + List responders = new ArrayList<>(); + for (ScanResult result : wifiScanList) { + if (result != null && result.BSSID != null && result.is80211mcResponder()) { + responders.add(result); + } + if (responders.size() >= 8) { + break; + } + } - //} + if (responders.isEmpty()) { + return; + } + + isRttRanging = true; + RangingRequest request = new RangingRequest.Builder() + .addAccessPoints(responders) + .build(); + + try { + wifiRttManager.startRanging(request, context.getMainExecutor(), new RangingResultCallback() { + @Override + public void onRangingResults(List results) { + isRttRanging = false; + applyRttAltitudeResults(results); + } + + @Override + public void onRangingFailure(int code) { + isRttRanging = false; + Log.w("WifiDataProcessor", "Wi-Fi RTT ranging failed: " + code); + } + }); + } catch (SecurityException e) { + isRttRanging = false; + Log.w("WifiDataProcessor", "Wi-Fi RTT permission denied: " + e.getMessage()); + } catch (Exception e) { + isRttRanging = false; + Log.w("WifiDataProcessor", "Wi-Fi RTT unavailable: " + e.getMessage()); } } - /** - * Initiate scans for nearby networks every 5 seconds. - * The method declares a new timer instance to schedule a scan for nearby wifis every 5 seconds. - */ + private void applyRttAltitudeResults(List results) { + if (wifiData == null || results == null || results.isEmpty()) { + return; + } + + boolean updated = false; + for (RangingResult result : results) { + if (result == null || result.getStatus() != RangingResult.STATUS_SUCCESS) { + continue; + } + + ResponderLocation responderLocation = result.getUnverifiedResponderLocation(); + if (responderLocation == null) { + continue; + } + + double altitude; + try { + altitude = responderLocation.getAltitude(); + } catch (Exception e) { + continue; + } + if (Double.isNaN(altitude) || Double.isInfinite(altitude)) { + continue; + } + + MacAddress macAddress = result.getMacAddress(); + if (macAddress == null) { + continue; + } + + long bssid = convertBssidToLong(macAddress.toString()); + for (Wifi wifi : wifiData) { + if (wifi.getBssid() == bssid) { + wifi.setRttAltitudeMeters((float) altitude); + updated = true; + break; + } + } + } + + if (updated) { + notifyObservers(0); + } + } + + // Initiate scans for nearby networks every 5 seconds. public void startListening() { + // Cancel existing timer if any to avoid duplicates + if (this.scanWifiDataTimer != null) { + this.scanWifiDataTimer.cancel(); + } this.scanWifiDataTimer = new Timer(); this.scanWifiDataTimer.scheduleAtFixedRate(new scheduledWifiScan(), 0, scanInterval); } - /** - * Cancel wifi scans. - * The method unregisters the broadcast receiver associated with the wifi scans and cancels the - * timer so that new scans are not initiated. - */ + // Cancel wifi scans. public void stopListening() { - context.unregisterReceiver(wifiScanReceiver); - this.scanWifiDataTimer.cancel(); + // Safe unregister + try { + if (isReceiverRegistered) { + context.unregisterReceiver(wifiScanReceiver); + isReceiverRegistered = false; + } + } catch (IllegalArgumentException e) { + // Ignore if not registered + } + + if (this.scanWifiDataTimer != null) { + this.scanWifiDataTimer.cancel(); + this.scanWifiDataTimer = null; // Prevent reuse + } } - /** - * Inform user if throttling is resent on their device. - * If the device supports wifi throttling check if it is enabled and instruct the user to - * disable it. - */ + // Inform user if throttling is present. public void checkWifiThrottling(){ if(checkWifiPermissions()) { - //If the device does not support wifi throttling an exception is thrown try { - if(Settings.Global.getInt(context.getContentResolver(), "wifi_scan_throttle_enabled")==1) { - //Inform user to disable wifi throttling - Toast.makeText(context, "Disable Wi-Fi Throttling", Toast.LENGTH_SHORT).show(); + // Check if throttling is enabled (API 28+) + // Note: This setting might not be readable on all devices/versions without special permissions, + // but we keep the try-catch block as in original. + if(Settings.Global.getInt(context.getContentResolver(), "wifi_scan_throttle_enabled") == 1) { + Toast.makeText(context, "Disable Wi-Fi Throttling in Dev Options", Toast.LENGTH_LONG).show(); } } catch (Settings.SettingNotFoundException e) { - e.printStackTrace(); + // Setting not found, ignore } } } - /** - * Implement default method from Observable Interface to add new observers to the class. - * - * @param o Classes which implement the Observer interface to receive updates from the class. - */ @Override public void registerObserver(Observer o) { observers.add(o); } - /** - * Implement default method from Observable Interface to add notify observers to the class. - * Changes to the wifiData variable are passed to observers of the class. - * @param idx Unused. - */ @Override public void notifyObservers(int idx) { for(Observer o : observers) { - o.update(wifiData); + // Make a copy or pass the array. + // wifiData might be null if no scan has finished yet. + if (wifiData != null) { + o.update(wifiData); + } } } - /** - * Class to schedule wifi scans. - * - * Implements default method in {@link TimerTask} class which it implements. It begins to start - * calling wifi scans every 5 seconds. - */ private class scheduledWifiScan extends TimerTask { - @Override public void run() { startWifiScan(); } } - /** - * Obtains required information about wifi in which the device is currently connected. - * - * A connectivity manager is used to obtain information about the current network. If the device - * is connected to a network its ssid, mac address and frequency is stored to a Wifi object so - * that it can be accessed by the caller of the method - * - * @return wifi object containing the currently connected wifi's ssid, mac address and frequency - */ + // Obtains required information about wifi in which the device is currently connected. public Wifi getCurrentWifiData(){ - //Set up a connectivity manager to get information about the wifi ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService (Context.CONNECTIVITY_SERVICE); - //Set up a network info object to store information about the current network NetworkInfo networkInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); - //Only obtain wifi data if the device is connected - //Wifi in which the device is currently connected to Wifi currentWifi = new Wifi(); - if(networkInfo.isConnected()) { - //Store the ssid, mac address and frequency of the current wifi - currentWifi.setSsid(wifiManager.getConnectionInfo().getSSID()); - String wifiMacAddress = wifiManager.getConnectionInfo().getBSSID(); - long intMacAddress = convertBssidToLong(wifiMacAddress); - currentWifi.setBssid(intMacAddress); - currentWifi.setFrequency(wifiManager.getConnectionInfo().getFrequency()); + + if(networkInfo != null && networkInfo.isConnected()) { + // Store the ssid, mac address and frequency of the current wifi + // Use standard API safely + try { + android.net.wifi.WifiInfo info = wifiManager.getConnectionInfo(); + if (info != null) { + // SSID usually comes with quotes, keep them or strip them as needed. + // Original code kept them, so we keep them. + currentWifi.setSsid(info.getSSID()); + currentWifi.setBssid(convertBssidToLong(info.getBSSID())); + currentWifi.setFrequency(info.getFrequency()); + } + } catch (Exception e) { + Log.e("WifiDataProcessor", "Error getting connection info"); + } } - else{ - //Store standard information if not connected + else { currentWifi.setSsid("Not connected"); currentWifi.setBssid(0); currentWifi.setFrequency(0); @@ -330,3 +411,5 @@ public Wifi getCurrentWifiData(){ return currentWifi; } } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/BuildingPolygon.java b/app/src/main/java/com/openpositioning/PositionMe/utils/BuildingPolygon.java index 2d0a3265..771ddb4d 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/utils/BuildingPolygon.java +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/BuildingPolygon.java @@ -6,12 +6,10 @@ import java.util.ArrayList; import java.util.List; -/** - * Class used to check for a pre-defined set of coordinates if it is in a Building (Nucleus, Library) - * (Can be used to add more buildings by adding the coordinates of the buildings and adding methods) - * @see IndoorMapManager Used by the the IndoorFloorManager class - * @author Arun Gopalakrishnan - */ +// Class used to check for a pre-defined set of coordinates if it is in a Building (Nucleus, Library) +// (Can be used to add more buildings by adding the coordinates of the buildings and adding methods) +// Related: IndoorMapManager Used by the the IndoorFloorManager class +// @author Arun Gopalakrishnan public class BuildingPolygon { // Defining the coordinates of the building boundaries (rectangular boundaries based on floor map shape) // North-East and South-West Coordinates for the Nucleus Building @@ -28,40 +26,66 @@ public class BuildingPolygon { add(BuildingPolygon.NUCLEUS_SW); add(new LatLng(BuildingPolygon.NUCLEUS_NE.latitude, BuildingPolygon.NUCLEUS_SW.longitude)); // North-West }}; - //Boundary coordinates of the Library building (clockwise) + // Boundary coordinates of the Library building (clockwise) public static final List LIBRARY_POLYGON = new ArrayList() {{ add(BuildingPolygon.LIBRARY_NE); - add(new LatLng(BuildingPolygon.LIBRARY_SW.latitude,BuildingPolygon.LIBRARY_NE.longitude));//(South-East) + add(new LatLng(BuildingPolygon.LIBRARY_SW.latitude,BuildingPolygon.LIBRARY_NE.longitude));// (South-East) add(BuildingPolygon.LIBRARY_SW); - add(new LatLng(BuildingPolygon.LIBRARY_NE.latitude,BuildingPolygon.LIBRARY_SW.longitude));//(North-West) + 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 - * @return True if point is in Nucleus building else False - */ + public static final List MURCHISON_POLYGON = new ArrayList() {{ + add(new LatLng(55.92447, -3.17868)); + add(new LatLng(55.92379, -3.17868)); + add(new LatLng(55.92379, -3.17964)); + add(new LatLng(55.92447, -3.17964)); + }}; + + public static final List FJB_POLYGON = new ArrayList() {{ + add(new LatLng(55.92282, -3.17259)); + add(new LatLng(55.92221, -3.17192)); + add(new LatLng(55.92211, -3.17228)); + add(new LatLng(55.92269, -3.17296)); + }}; + + // Function to check if a point is in the Nucleus Building +// Parameter point: the point to be checked if inside the building +// Returns: True if point is in Nucleus building else False public static boolean inNucleus(LatLng point){ return (pointInPolygon(point,NUCLEUS_POLYGON)); } - /** - * Function to check if a point is in the Library Building - * @param point the point which is checked if inside the building - * @return True if point is in Library building else False - */ + // Function to check if a point is in the Library Building +// Parameter point: the point which is checked if inside the building +// Returns: True if point is in Library building else False public static boolean inLibrary(LatLng point){ return (pointInPolygon(point,LIBRARY_POLYGON)); } - /** - * Function to check if point in polygon (approximates earth to be flat) - * Ray casting algorithm https://en.wikipedia.org/wiki/Point_in_polygon - * @param point point to be checked if in polygon - * @param polygon Boundaries of the building - * @return True if point in polygon - * False otherwise - */ + public static boolean inMurchison(LatLng point) { + return pointInPolygon(point, MURCHISON_POLYGON); + } + + public static boolean inFjb(LatLng point) { + return pointInPolygon(point, FJB_POLYGON); + } + + public static boolean inAnyKnownBuilding(LatLng point) { + if (point == null) { + return false; + } + return inNucleus(point) + || inLibrary(point) + || inMurchison(point) + || inFjb(point); + } + + // Function to check if point in polygon (approximates earth to be flat) + // Ray casting algorithm https://en.wikipedia.org/wiki/Point_in_polygon +// Parameter point: point to be checked if in polygon +// Parameter polygon: Boundaries of the building +// Returns: True if point in polygon + // False otherwise private static boolean pointInPolygon(LatLng point, List polygon) { int numCrossings = 0; List path=polygon; @@ -79,20 +103,18 @@ private static boolean pointInPolygon(LatLng point, List polygon) { } } - //if odd number of numCrossings return true (point is in polygon) + // if odd number of numCrossings return true (point is in polygon) return (numCrossings % 2 == 1); } - /** - * Ray Casting algorithm for a segment joining ab - * @param point the point we check - * @param a the line segment's starting point - * @param b the line segment's ending point - * @return True if the point is - * 1) To the left of the segment ab - * 2) Not above nor below the segment ab - * Otherwise False - */ + // Ray Casting algorithm for a segment joining ab +// Parameter point: the point we check +// Parameter a: the line segment's starting point +// Parameter b: the line segment's ending point +// Returns: True if the point is + // To the left of the segment ab + // Not above nor below the segment ab + // Otherwise False private static boolean crossingSegment(LatLng point, LatLng a,LatLng b) { double pointLng = point.longitude, pointLat = point.latitude, @@ -111,7 +133,7 @@ private static boolean crossingSegment(LatLng point, LatLng a,LatLng b) { // If point has same latitude as a or b, increase slightly pointLat if (pointLat == aLat || pointLat == bLat) pointLat += 0.00000001; - //If the point is above, below or to the right of the segment,return false + // If the point is above, below or to the right of the segment,return false if ((pointLat > bLat || pointLat < aLat) || (pointLng > Math.max(aLng, bLng))){ return false; } @@ -128,3 +150,5 @@ else if (pointLng < Math.min(aLng, bLng)){ } } } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/CircularFloatBuffer.java b/app/src/main/java/com/openpositioning/PositionMe/utils/CircularFloatBuffer.java index 73abb674..1ac16491 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/utils/CircularFloatBuffer.java +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/CircularFloatBuffer.java @@ -5,11 +5,8 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; -/** - * Ring buffer for floats that can constantly update values in a fixed sized array. - * - * @author Mate Stodulka - */ +// Ring buffer for floats that can constantly update values in a fixed sized array. +// @author Mate Stodulka public class CircularFloatBuffer { // Default capacity for the buffer in case initial capacity is invalid private static final int DEFAULT_CAPACITY = 10; @@ -19,11 +16,8 @@ public class CircularFloatBuffer { private final float[] data; private volatile int writeSequence, readSequence; - /** - * Default constructor for a Circular Float Buffer with a given capacity. - * - * @param capacity size of the array. - */ + // Default constructor for a Circular Float Buffer with a given capacity. +// Parameter capacity: size of the array. public CircularFloatBuffer(int capacity) { this.capacity = (capacity < 1) ? DEFAULT_CAPACITY : capacity; this.data = new float[capacity]; @@ -31,13 +25,10 @@ public CircularFloatBuffer(int capacity) { this.writeSequence = -1; } - /** - * Put in a new element to the array. - * Overwrites the existing values if present already and moves the write head forward. - * - * @param element float value to be added to the array. - * @return true if adding to the element was successful. - */ + // Put in a new element to the array. + // Overwrites the existing values if present already and moves the write head forward. +// Parameter element: float value to be added to the array. +// Returns: true if adding to the element was successful. public boolean putNewest(float element) { int nextWriteSeq = writeSequence + 1; data[nextWriteSeq % capacity] = element; @@ -45,14 +36,10 @@ public boolean putNewest(float element) { return true; } - /** - * Get the oldest element in the array. - * If empty, return an empty Optional. Moves the read head forward. - * - * @return Optional float of the oldest element. - * - * @see Optional - */ + // Get the oldest element in the array. + // If empty, return an empty Optional. Moves the read head forward. +// Returns: Optional float of the oldest element. +// Related: Optional public Optional getOldest() { if (!isEmpty()) { float nextValue = data[readSequence % capacity]; @@ -62,48 +49,33 @@ public Optional getOldest() { return Optional.empty(); } - /** - * Get the capacity of the buffer. - * - * @return int capacity, size of the underlying array. - */ + // Get the capacity of the buffer. +// Returns: int capacity, size of the underlying array. public int getCapacity() { return capacity; } - /** - * Get the number of elements currently in the buffer. - * - * @return int number of floats in the buffer. - */ + // Get the number of elements currently in the buffer. +// Returns: int number of floats in the buffer. public int getCurrentSize() { return (writeSequence - readSequence) + 1; } - /** - * Checks if the buffer is empty. - * - * @return true if there are no elements in the buffer, false otherwise - */ + // Checks if the buffer is empty. +// Returns: true if there are no elements in the buffer, false otherwise public boolean isEmpty() { return writeSequence < readSequence; } - /** - * Check if the buffer is full. - * - * @return true if the number of elements in the buffer matches the capacity, false otherwise. - */ + // Check if the buffer is full. +// Returns: true if the number of elements in the buffer matches the capacity, false otherwise. public boolean isFull() { return getCurrentSize() >= capacity; } - /** - * Get a copy of the buffer as a list starting with the oldest element. - * If the list is not full return null. - * - * @return List of Floats contained in the buffer from oldest to newest. - */ + // Get a copy of the buffer as a list starting with the oldest element. + // If the list is not full return null. +// Returns: List of Floats contained in the buffer from oldest to newest. public List getListCopy() { if(!isFull()) return null; return IntStream.range(readSequence, readSequence + capacity) @@ -112,3 +84,5 @@ public List getListCopy() { } } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/GeometryUtils.java b/app/src/main/java/com/openpositioning/PositionMe/utils/GeometryUtils.java new file mode 100644 index 00000000..7ae953ad --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/GeometryUtils.java @@ -0,0 +1,209 @@ +package com.openpositioning.PositionMe.utils; + +import com.google.android.gms.maps.model.LatLng; +import java.util.List; + +// GeometryUtils - Utility class for geometric calculations +// Used for indoor navigation constraint checking (wall collision, boundary detection, etc.) +public class GeometryUtils { + + // Check if a point is inside a polygon using ray casting algorithm + public static boolean isPointInPolygon(LatLng point, List polygon) { + if (polygon == null || polygon.size() < 3) return false; + + boolean inside = false; + int j = polygon.size() - 1; + + for (int i = 0; i < polygon.size(); i++) { + LatLng pi = polygon.get(i); + LatLng pj = polygon.get(j); + + if ((pi.longitude > point.longitude) != (pj.longitude > point.longitude) && + (point.latitude < (pj.latitude - pi.latitude) * (point.longitude - pi.longitude) / + (pj.longitude - pi.longitude) + pi.latitude)) { + inside = !inside; + } + j = i; + } + return inside; + } + + // Calculate distance between two LatLng points in meters (Haversine formula) + public static double distanceBetween(LatLng p1, LatLng p2) { + final double R = 6371000; // Earth radius in meters + double lat1 = Math.toRadians(p1.latitude); + double lat2 = Math.toRadians(p2.latitude); + double dLat = Math.toRadians(p2.latitude - p1.latitude); + double dLon = Math.toRadians(p2.longitude - p1.longitude); + + double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(lat1) * Math.cos(lat2) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return R * c; + } + + // Check if a line segment (from -> to) crosses any wall line segment + // Returns true if movement would cross a wall + public static boolean crossesWall(LatLng from, LatLng to, List> walls) { + if (walls == null || walls.isEmpty()) return false; + + for (List wall : walls) { + if (wall.size() < 2) continue; + + // Check each segment of the wall polyline + for (int i = 0; i < wall.size() - 1; i++) { + if (lineSegmentsIntersect(from, to, wall.get(i), wall.get(i + 1))) { + return true; + } + } + } + return false; + } + + // Check if two line segments intersect + private static boolean lineSegmentsIntersect(LatLng p1, LatLng p2, LatLng p3, LatLng p4) { + double d1 = direction(p3, p4, p1); + double d2 = direction(p3, p4, p2); + double d3 = direction(p1, p2, p3); + double d4 = direction(p1, p2, p4); + + if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && + ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) { + return true; + } + + // Check for collinear cases + if (d1 == 0 && onSegment(p3, p1, p4)) return true; + if (d2 == 0 && onSegment(p3, p2, p4)) return true; + if (d3 == 0 && onSegment(p1, p3, p2)) return true; + if (d4 == 0 && onSegment(p1, p4, p2)) return true; + + return false; + } + + // Calculate direction (cross product) + private static double direction(LatLng p1, LatLng p2, LatLng p3) { + return (p3.latitude - p1.latitude) * (p2.longitude - p1.longitude) - + (p2.latitude - p1.latitude) * (p3.longitude - p1.longitude); + } + + // Check if point q lies on segment pr + private static boolean onSegment(LatLng p, LatLng q, LatLng r) { + return q.latitude <= Math.max(p.latitude, r.latitude) && + q.latitude >= Math.min(p.latitude, r.latitude) && + q.longitude <= Math.max(p.longitude, r.longitude) && + q.longitude >= Math.min(p.longitude, r.longitude); + } + + // Find the closest valid point inside a polygon boundary + // Used when detected position is outside building + public static LatLng constrainToPolygon(LatLng point, List polygon) { + if (isPointInPolygon(point, polygon)) { + return point; // Already inside + } + + // Find closest point on polygon perimeter + LatLng closest = null; + double minDistance = Double.MAX_VALUE; + + for (int i = 0; i < polygon.size(); i++) { + LatLng p1 = polygon.get(i); + LatLng p2 = polygon.get((i + 1) % polygon.size()); + LatLng nearestOnSegment = closestPointOnSegment(point, p1, p2); + + double dist = distanceBetween(point, nearestOnSegment); + if (dist < minDistance) { + minDistance = dist; + closest = nearestOnSegment; + } + } + + // Move slightly inward from boundary (0.5 meters) + if (closest != null) { + LatLng center = getPolygonCenter(polygon); + double dx = (center.latitude - closest.latitude) * 0.00001; // ~1 meter + double dy = (center.longitude - closest.longitude) * 0.00001; + return new LatLng(closest.latitude + dx, closest.longitude + dy); + } + + return point; // Fallback + } + + // Find closest point on a line segment to a given point + private static LatLng closestPointOnSegment(LatLng point, LatLng segStart, LatLng segEnd) { + double dx = segEnd.latitude - segStart.latitude; + double dy = segEnd.longitude - segStart.longitude; + + if (dx == 0 && dy == 0) return segStart; + + double t = ((point.latitude - segStart.latitude) * dx + + (point.longitude - segStart.longitude) * dy) / (dx * dx + dy * dy); + + t = Math.max(0, Math.min(1, t)); // Clamp to [0,1] + + return new LatLng( + segStart.latitude + t * dx, + segStart.longitude + t * dy + ); + } + + // Calculate polygon center (centroid) + private static LatLng getPolygonCenter(List polygon) { + double sumLat = 0, sumLon = 0; + for (LatLng p : polygon) { + sumLat += p.latitude; + sumLon += p.longitude; + } + return new LatLng(sumLat / polygon.size(), sumLon / polygon.size()); + } + + // Smooth trajectory using exponential moving average + // alpha = smoothing factor (0-1), higher = less smoothing + public static LatLng smoothPosition(LatLng newPos, LatLng prevPos, double alpha) { + if (prevPos == null) return newPos; + + double smoothLat = alpha * newPos.latitude + (1 - alpha) * prevPos.latitude; + double smoothLon = alpha * newPos.longitude + (1 - alpha) * prevPos.longitude; + + return new LatLng(smoothLat, smoothLon); + } + + // Detect if position jump is unrealistic (teleportation) + // maxSpeed in meters/second + public static boolean isUnrealisticJump(LatLng from, LatLng to, long deltaTimeMs, double maxSpeed) { + if (from == null || to == null || deltaTimeMs <= 0) return false; + + double distance = distanceBetween(from, to); + double speed = distance / (deltaTimeMs / 1000.0); // m/s + + return speed > maxSpeed; // Typically 2-3 m/s for walking + } + + // Check whether a point lies inside or close to a feature polyline/polygon. + public static boolean isPointNearFeature(LatLng point, List feature, double radiusMeters) { + if (point == null || feature == null || feature.isEmpty()) { + return false; + } + + if (feature.size() >= 3 && isPointInPolygon(point, feature)) { + return true; + } + + if (feature.size() == 1) { + return distanceBetween(point, feature.get(0)) <= radiusMeters; + } + + for (int i = 0; i < feature.size() - 1; i++) { + LatLng closestPoint = closestPointOnSegment(point, feature.get(i), feature.get(i + 1)); + if (distanceBetween(point, closestPoint) <= radiusMeters) { + return true; + } + } + + return false; + } +} + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/ImprovedMadgwickAHRS.java b/app/src/main/java/com/openpositioning/PositionMe/utils/ImprovedMadgwickAHRS.java new file mode 100644 index 00000000..55026276 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/ImprovedMadgwickAHRS.java @@ -0,0 +1,314 @@ +package com.openpositioning.PositionMe.utils; + +import android.hardware.SensorManager; +import android.util.Log; + + + + + + +public class ImprovedMadgwickAHRS { + + private static final float DEFAULT_BETA = 0.15f; + private static final float MAGNETOMETER_MIN = 20.0f; + private static final float MAGNETOMETER_MAX = 60.0f; + private static final float GRAVITY_NOMINAL = 9.81f; + + private float beta; + private float q0 = 1.0f; + private float q1 = 0.0f; + private float q2 = 0.0f; + private float q3 = 0.0f; + + + private CircularFloatBuffer magIntensityBuffer; + private int magAnomalyCount = 0; + private static final int MAG_ANOMALY_THRESHOLD = 5; + + public ImprovedMadgwickAHRS() { + this(DEFAULT_BETA); + this.magIntensityBuffer = new CircularFloatBuffer(10); + } + + public ImprovedMadgwickAHRS(float beta) { + this.beta = beta; + this.magIntensityBuffer = new CircularFloatBuffer(10); + } + + public void setBeta(float beta) { + this.beta = beta; + } + + public void reset() { + q0 = 1.0f; + q1 = 0.0f; + q2 = 0.0f; + q3 = 0.0f; + magAnomalyCount = 0; + } + + + public void update(float gx, float gy, float gz, + float ax, float ay, float az, + float mx, float my, float mz, + float deltaTimeSeconds) { + if (deltaTimeSeconds <= 0.0f) { + return; + } + + + float accNorm = invSqrt(ax * ax + ay * ay + az * az); + if (Float.isNaN(accNorm) || Float.isInfinite(accNorm)) { + integrateGyroscopeOnly(gx, gy, gz, deltaTimeSeconds); + return; + } + ax *= accNorm; + ay *= accNorm; + az *= accNorm; + + + float magIntensity = (float) Math.sqrt(mx * mx + my * my + mz * mz); + + + boolean isMagneticAnomaly = isMagneticAnomaly(magIntensity); + + if (isMagneticAnomaly) { + + Log.w("MadgwickAHRS", "Magnetic anomaly detected: " + magIntensity + " 渭T"); + integrateGyroAndAccel(gx, gy, gz, ax, ay, az, deltaTimeSeconds); + magAnomalyCount++; + return; + } + + + float magNorm = invSqrt(mx * mx + my * my + mz * mz); + if (Float.isNaN(magNorm) || Float.isInfinite(magNorm)) { + integrateGyroscopeOnly(gx, gy, gz, deltaTimeSeconds); + return; + } + mx *= magNorm; + my *= magNorm; + mz *= magNorm; + magAnomalyCount = 0; + + + float accelMagnitude = (float) Math.sqrt(ax * ax + ay * ay + az * az); + float adaptiveBeta = calculateAdaptiveBeta(accelMagnitude); + + + updateWithFullMagneticCorrection(gx, gy, gz, ax, ay, az, mx, my, mz, adaptiveBeta); + } + + + + private boolean isMagneticAnomaly(float magIntensity) { + magIntensityBuffer.putNewest(magIntensity); + + + if (magIntensity < MAGNETOMETER_MIN || magIntensity > MAGNETOMETER_MAX) { + return true; + } + + + if (magIntensityBuffer.isFull()) { + java.util.List history = magIntensityBuffer.getListCopy(); + float mean = 0; + for (float val : history) { + mean += val; + } + mean /= history.size(); + + float variance = 0; + for (float val : history) { + variance += (val - mean) * (val - mean); + } + variance /= history.size(); + float stdDev = (float) Math.sqrt(variance); + + + if (Math.abs(magIntensity - mean) > 2.0f * stdDev) { + return true; + } + } + + return false; + } + + + + private float calculateAdaptiveBeta(float accelMagnitude) { + + float deviation = Math.abs(accelMagnitude - GRAVITY_NOMINAL); + + if (deviation > 3.0f) { + + return 0.30f; + } else if (deviation > 1.0f) { + + return 0.20f; + } else if (deviation < 0.2f) { + + return 0.10f; + } else { + + return 0.15f; + } + } + + + private void integrateGyroAndAccel(float gx, float gy, float gz, + float ax, float ay, float az, + float deltaTimeSeconds) { + float q0q0 = q0 * q0; + float q0q1 = q0 * q1; + float q0q2 = q0 * q2; + float q0q3 = q0 * q3; + float q1q1 = q1 * q1; + float q1q2 = q1 * q2; + float q1q3 = q1 * q3; + float q2q2 = q2 * q2; + float q2q3 = q2 * q3; + float q3q3 = q3 * q3; + + + float gx_pred = 2.0f * (q1q3 - q0q2); + float gy_pred = 2.0f * (q0q1 + q2q3); + float gz_pred = q0q0 - q1q1 - q2q2 + q3q3; + + + float ex = ay * gz_pred - az * gy_pred; + float ey = az * gx_pred - ax * gz_pred; + float ez = ax * gy_pred - ay * gx_pred; + + + float adaptiveBeta = calculateAdaptiveBeta((float) Math.sqrt(ax * ax + ay * ay + az * az)); + + float qDot0 = 0.5f * (-q1 * gx - q2 * gy - q3 * gz) - adaptiveBeta * ex; + float qDot1 = 0.5f * (q0 * gx + q2 * gz - q3 * gy) - adaptiveBeta * ey; + float qDot2 = 0.5f * (q0 * gy - q1 * gz + q3 * gx) - adaptiveBeta * ez; + float qDot3 = 0.5f * (q0 * gz + q1 * gy - q2 * gx); + + q0 += qDot0 * deltaTimeSeconds; + q1 += qDot1 * deltaTimeSeconds; + q2 += qDot2 * deltaTimeSeconds; + q3 += qDot3 * deltaTimeSeconds; + + normalizeQuaternion(); + } + + + private void integrateGyroscopeOnly(float gx, float gy, float gz, float deltaTimeSeconds) { + float qDot0 = 0.5f * (-q1 * gx - q2 * gy - q3 * gz); + float qDot1 = 0.5f * (q0 * gx + q2 * gz - q3 * gy); + float qDot2 = 0.5f * (q0 * gy - q1 * gz + q3 * gx); + float qDot3 = 0.5f * (q0 * gz + q1 * gy - q2 * gx); + + q0 += qDot0 * deltaTimeSeconds; + q1 += qDot1 * deltaTimeSeconds; + q2 += qDot2 * deltaTimeSeconds; + q3 += qDot3 * deltaTimeSeconds; + normalizeQuaternion(); + } + + + private void updateWithFullMagneticCorrection(float gx, float gy, float gz, + float ax, float ay, float az, + float mx, float my, float mz, + float adaptiveBeta) { + float q0q0 = q0 * q0; + float q0q1 = q0 * q1; + float q0q2 = q0 * q2; + float q0q3 = q0 * q3; + float q1q1 = q1 * q1; + float q1q2 = q1 * q2; + float q1q3 = q1 * q3; + float q2q2 = q2 * q2; + float q2q3 = q2 * q3; + float q3q3 = q3 * q3; + + + float hx = 2.0f * mx * (0.5f - q2q2 - q3q3) + + 2.0f * my * (q1q2 - q0q3) + + 2.0f * mz * (q1q3 + q0q2); + float hy = 2.0f * mx * (q1q2 + q0q3) + + 2.0f * my * (0.5f - q1q1 - q3q3) + + 2.0f * mz * (q2q3 - q0q1); + float bx = (float) Math.sqrt(hx * hx + hy * hy); + float bz = 2.0f * mx * (q1q3 - q0q2) + + 2.0f * my * (q2q3 + q0q1) + + 2.0f * mz * (0.5f - q1q1 - q2q2); + + + float s0 = -2.0f * q2 * (2.0f * q1q3 - 2.0f * q0q2 - ax) + + 2.0f * q1 * (2.0f * q0q1 + 2.0f * q2q3 - ay) + - 2.0f * bz * q2 * (2.0f * bx * (0.5f - q2q2 - q3q3) + 2.0f * bz * (q1q3 - q0q2) - mx) + + (-2.0f * bx * q3 + 2.0f * bz * q1) * (2.0f * bx * (q1q2 - q0q3) + 2.0f * bz * (q0q1 + q2q3) - my) + + 2.0f * bx * q2 * (2.0f * bx * (q0q2 + q1q3) + 2.0f * bz * (0.5f - q1q1 - q2q2) - mz); + float s1 = 2.0f * q3 * (2.0f * q1q3 - 2.0f * q0q2 - ax) + + 2.0f * q0 * (2.0f * q0q1 + 2.0f * q2q3 - ay) + - 4.0f * q1 * (1.0f - 2.0f * q1q1 - 2.0f * q2q2 - az) + + 2.0f * bz * q3 * (2.0f * bx * (0.5f - q2q2 - q3q3) + 2.0f * bz * (q1q3 - q0q2) - mx) + + (2.0f * bx * q2 + 2.0f * bz * q0) * (2.0f * bx * (q1q2 - q0q3) + 2.0f * bz * (q0q1 + q2q3) - my) + + (2.0f * bx * q3 - 4.0f * bz * q1) * (2.0f * bx * (q0q2 + q1q3) + 2.0f * bz * (0.5f - q1q1 - q2q2) - mz); + float s2 = -2.0f * q0 * (2.0f * q1q3 - 2.0f * q0q2 - ax) + + 2.0f * q3 * (2.0f * q0q1 + 2.0f * q2q3 - ay) + - 4.0f * q2 * (1.0f - 2.0f * q1q1 - 2.0f * q2q2 - az) + + (-4.0f * bx * q2 - 2.0f * bz * q0) * (2.0f * bx * (0.5f - q2q2 - q3q3) + 2.0f * bz * (q1q3 - q0q2) - mx) + + (2.0f * bx * q1 + 2.0f * bz * q3) * (2.0f * bx * (q1q2 - q0q3) + 2.0f * bz * (q0q1 + q2q3) - my) + + (2.0f * bx * q0 - 4.0f * bz * q2) * (2.0f * bx * (q0q2 + q1q3) + 2.0f * bz * (0.5f - q1q1 - q2q2) - mz); + float s3 = 2.0f * q1 * (2.0f * q1q3 - 2.0f * q0q2 - ax) + + 2.0f * q2 * (2.0f * q0q1 + 2.0f * q2q3 - ay) + + (-4.0f * bx * q3 + 2.0f * bz * q1) * (2.0f * bx * (0.5f - q2q2 - q3q3) + 2.0f * bz * (q1q3 - q0q2) - mx) + + (-2.0f * bx * q0 + 2.0f * bz * q2) * (2.0f * bx * (q1q2 - q0q3) + 2.0f * bz * (q0q1 + q2q3) - my) + + 2.0f * bx * q1 * (2.0f * bx * (q0q2 + q1q3) + 2.0f * bz * (0.5f - q1q1 - q2q2) - mz); + + float stepNorm = invSqrt(s0 * s0 + s1 * s1 + s2 * s2 + s3 * s3); + if (!Float.isNaN(stepNorm) && !Float.isInfinite(stepNorm)) { + s0 *= stepNorm; + s1 *= stepNorm; + s2 *= stepNorm; + s3 *= stepNorm; + } + + float qDot0 = 0.5f * (-q1 * gx - q2 * gy - q3 * gz) - adaptiveBeta * s0; + float qDot1 = 0.5f * (q0 * gx + q2 * gz - q3 * gy) - adaptiveBeta * s1; + float qDot2 = 0.5f * (q0 * gy - q1 * gz + q3 * gx) - adaptiveBeta * s2; + float qDot3 = 0.5f * (q0 * gz + q1 * gy - q2 * gx) - adaptiveBeta * s3; + + q0 += qDot0 * 0.01f; + q1 += qDot1 * 0.01f; + q2 += qDot2 * 0.01f; + q3 += qDot3 * 0.01f; + + normalizeQuaternion(); + } + + private void normalizeQuaternion() { + float quaternionNorm = invSqrt(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3); + if (Float.isNaN(quaternionNorm) || Float.isInfinite(quaternionNorm)) { + return; + } + q0 *= quaternionNorm; + q1 *= quaternionNorm; + q2 *= quaternionNorm; + q3 *= quaternionNorm; + } + + private static float invSqrt(float x) { + return 1.0f / (float) Math.sqrt(x); + } + + public void getQuaternion(float[] q) { + q[0] = q0; + q[1] = q1; + q[2] = q2; + q[3] = q3; + } + + public int getMagneticAnomalyCount() { + return magAnomalyCount; + } +} + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/IndoorBuilding.java b/app/src/main/java/com/openpositioning/PositionMe/utils/IndoorBuilding.java new file mode 100644 index 00000000..21e6296d --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/IndoorBuilding.java @@ -0,0 +1,34 @@ +package com.openpositioning.PositionMe.utils; + +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.LatLngBounds; +import java.util.List; +import java.util.Map; + +// Stores building info from API. +public class IndoorBuilding { + public String id; + public String name; + public List polygonPoints; // Building outline + public LatLngBounds bounds; // Image coverage bounds + public Map floorUrls; // Floor to image URL map (e.g. 0 -> "http://.../g_floor.png") + public float floorHeight; // Floor height + public float[] floorAltitudeAnchorsMeters; // Optional absolute altitude anchors per floor + + public IndoorBuilding(String id, String name, List polygonPoints, LatLngBounds bounds, Map floorUrls, float floorHeight) { + this(id, name, polygonPoints, bounds, floorUrls, floorHeight, null); + } + + public IndoorBuilding(String id, String name, List polygonPoints, LatLngBounds bounds, + Map floorUrls, float floorHeight, float[] floorAltitudeAnchorsMeters) { + this.id = id; + this.name = name; + this.polygonPoints = polygonPoints; + this.bounds = bounds; + this.floorUrls = floorUrls; + this.floorHeight = floorHeight; + this.floorAltitudeAnchorsMeters = floorAltitudeAnchorsMeters; + } +} + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/IndoorMapManager.java b/app/src/main/java/com/openpositioning/PositionMe/utils/IndoorMapManager.java index 9d7167df..0c796fbc 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/utils/IndoorMapManager.java +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/IndoorMapManager.java @@ -1,193 +1,1556 @@ package com.openpositioning.PositionMe.utils; +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Color; +import android.os.Handler; +import android.os.Looper; import android.util.Log; +import androidx.preference.PreferenceManager; + import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.model.BitmapDescriptorFactory; import com.google.android.gms.maps.model.GroundOverlay; import com.google.android.gms.maps.model.GroundOverlayOptions; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; +import com.google.android.gms.maps.model.Polygon; +import com.google.android.gms.maps.model.PolygonOptions; import com.google.android.gms.maps.model.PolylineOptions; -import com.openpositioning.PositionMe.R; +import com.openpositioning.PositionMe.BuildConfig; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; -import java.util.Arrays; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.regex.Matcher; +import java.util.regex.Pattern; -/** - * Class used to manage indoor floor map overlays - * Currently used by RecordingFragment - * @see BuildingPolygon Describes the bounds of buildings and the methods to check if point is - * in the building - * @author Arun Gopalakrishnan - */ +// IndoorMapManager - API-based indoor map handler. +// Supports GeoJSON data parsing and floor plan management. public class IndoorMapManager { - // To store the map instance + + private static final String TAG = "IndoorMapManager"; + private static final float[] NUCLEUS_FLOOR_ALTITUDE_ANCHORS = new float[]{127.0f, 130.0f, 135.5f, 139.5f, 145.9f}; + private static final Pattern FLOOR_NUMBER_PATTERN = Pattern.compile("-?\\d+"); + private static final int FEATURE_KIND_DEFAULT = 0; + private static final int FEATURE_KIND_WALL = 1; + private static final int FEATURE_KIND_STAIRS = 2; + private static final int FEATURE_KIND_LIFT = 3; + + private static final int COLOR_FEATURE_DEFAULT_STROKE = Color.argb(210, 125, 135, 150); + private static final int COLOR_FEATURE_DEFAULT_FILL = Color.argb(40, 125, 135, 150); + private static final int COLOR_FEATURE_WALL_STROKE = Color.argb(235, 32, 37, 43); + private static final int COLOR_FEATURE_WALL_FILL = Color.TRANSPARENT; + private static final int COLOR_FEATURE_STAIRS_STROKE = Color.argb(235, 255, 149, 0); + private static final int COLOR_FEATURE_STAIRS_FILL = Color.argb(72, 255, 149, 0); + private static final int COLOR_FEATURE_LIFT_STROKE = Color.argb(235, 0, 122, 255); + private static final int COLOR_FEATURE_LIFT_FILL = Color.argb(72, 0, 122, 255); private GoogleMap gMap; - //Stores the overlay of the indoor maps + private Context context; private GroundOverlay groundOverlay; - // Stores the current Location of user + private SharedPreferences settings; + + // Current location private LatLng currentLocation; - // Stores if indoor map overlay is currently set - private boolean isIndoorMapSet=false; - //Stores the current floor in building - private int currentFloor; - // Floor height of current building - private float floorHeight; - //Images of the Nucleus Building and Library indoor floor maps - 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.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( - BuildingPolygon.NUCLEUS_SW, - BuildingPolygon.NUCLEUS_NE - ); - 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; - - /** - * Constructor to set the map instance - * @param map The map on which the indoor floor map overlays are set - */ - public IndoorMapManager(GoogleMap map){ - this.gMap=map; - } - - /** - * Function to update the current location of user and display the indoor map - * if user in building with indoor map available - * @param currentLocation new location of user - */ - public void setCurrentLocation(LatLng currentLocation){ - this.currentLocation=currentLocation; - setBuildingOverlay(); - } - - /** - * Function to obtain the current building's floor height - * @return the floor height of the current building the user is in - */ - public float getFloorHeight() { - return floorHeight; - } - - /** - * Getter to obtain if currently an indoor floor map is being displayed - * @return true if an indoor map is visible to the user, false otherwise - */ - public boolean getIsIndoorMapSet(){ - return isIndoorMapSet; - } - - /** - * Setting the new floor of a user and displaying the indoor floor map accordingly - * (if floor exists in building) - * @param newFloor the floor the user is at - * @param autoFloor flag if function called by auto-floor feature - */ - public void setCurrentFloor(int newFloor, boolean autoFloor) { - if (BuildingPolygon.inNucleus(currentLocation)){ - //Special case for nucleus when auto-floor is being used - if (autoFloor) { - // If nucleus add bias floor as lower-ground floor referred to as floor 0 - newFloor += 1; - } - // If within bounds and different from floor map currently being shown - if (newFloor>=0 && newFloor=0 && newFloor drawnShapes = new ArrayList<>(); + + private IndoorBuilding selectedBuilding; + private int currentFloor = 0; + private boolean isIndoorMapSet = false; + private boolean isIndoorMapVisible = true; // Indoor map visibility toggle + + // Store floor data from API response + private String currentVenueName = null; + private String currentOutlineGeoJson = null; + private Map floorShapesMap = new HashMap<>(); // floor name -> GeoJSON string + private List floorNamesList = new ArrayList<>(); // Ordered list of floor names + + // Wall collision detection data + private Map>> floorWallsMap = new HashMap<>(); // floor name -> wall polylines + private Map>> floorStairsMap = new HashMap<>(); // floor name -> stairs geometries + private Map>> floorLiftsMap = new HashMap<>(); // floor name -> lift geometries + private List buildingBoundary = null; // Current building boundary polygon + + private Map polygonMap = new HashMap<>(); + private final List fallbackBuildings = new ArrayList<>(); + + // Request tracking to handle race conditions + private int currentRequestId = 0; + private String pendingBuildingName = null; // Building name for current pending request + + private static final String BASE_URL = "https://openpositioning.org/api/live/floorplan/request/"; + private static final String RAW_API_KEY = BuildConfig.OPENPOSITIONING_API_KEY; + private static final String RAW_MASTER_KEY = BuildConfig.OPENPOSITIONING_MASTER_KEY; + + private ExecutorService executor = Executors.newSingleThreadExecutor(); + private Handler mainHandler = new Handler(Looper.getMainLooper()); + + // Callback for when floor data is loaded + public interface OnFloorDataLoadedListener { + void onFloorDataLoaded(boolean hasData); + } + private OnFloorDataLoadedListener floorDataLoadedListener; + + public IndoorMapManager(GoogleMap map, Context context) { + this.gMap = map; + this.context = context; + this.settings = PreferenceManager.getDefaultSharedPreferences(context); + } + + // Core API methods + + public void fetchFloorPlan(LatLng loc, List macs) { + // Increment request ID and save expected building name + final int thisRequestId = ++currentRequestId; + final String expectedBuildingName = (selectedBuilding != null) ? selectedBuilding.name : "unknown"; + pendingBuildingName = expectedBuildingName; + + Log.d(TAG, ">>> [REQUEST #" + thisRequestId + "] Starting for: " + expectedBuildingName); + + executor.execute(() -> { + try { + String cleanApiKey = RAW_API_KEY.replace("<", "").replace(">", "").trim(); + String cleanMasterKey = RAW_MASTER_KEY.replace("<", "").replace(">", "").trim(); + String urlString = BASE_URL + cleanApiKey + "?key=" + cleanMasterKey; + + Log.d(TAG, ">>> [API] Requesting: " + urlString); + Log.d(TAG, ">>> [API] Request body: lat=" + String.format("%.6f", loc.latitude) + ", lon=" + String.format("%.6f", loc.longitude)); + + URL url = new URL(urlString); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); + conn.setConnectTimeout(10000); + + JSONObject jsonBody = new JSONObject(); + jsonBody.put("lat", loc.latitude); + jsonBody.put("lon", loc.longitude); + JSONArray macArray = new JSONArray(); + if (macs != null) for (String mac : macs) macArray.put(mac); + jsonBody.put("macs", macArray); + + try (OutputStream os = conn.getOutputStream()) { + byte[] input = jsonBody.toString().getBytes(StandardCharsets.UTF_8); + os.write(input, 0, input.length); + } + + int responseCode = conn.getResponseCode(); + Log.d(TAG, ">>> [API] Response code: " + responseCode); + + if (responseCode == 200) { + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); + StringBuilder responseSb = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) responseSb.append(line); + String jsonResponse = responseSb.toString().trim(); + + Log.d(TAG, ">>> [API] Response received (" + jsonResponse.length() + " chars)"); + Log.d(TAG, ">>> [API] Full response: " + jsonResponse); + + // Clear previous map + mainHandler.post(this::hideMap); + + // Check if this response is still relevant (not superseded by newer request) + if (thisRequestId != currentRequestId) { + Log.w(TAG, ">>> [REQUEST #" + thisRequestId + "] IGNORED - superseded by request #" + currentRequestId); + return; + } + + // Parse response + if (jsonResponse.startsWith("[")) { + JSONArray arr = new JSONArray(jsonResponse); + Log.d(TAG, ">>> [API] Response is an array with " + arr.length() + " elements"); + if (arr.length() > 0) { + parseResponseObject(arr.getJSONObject(0), expectedBuildingName); + } else { + Log.w(TAG, ">>> [API] Empty list [] returned. No map data for this location."); + Log.w(TAG, ">>> Server may not have indoor map data for this building."); + Log.w(TAG, ">>> Building: " + (selectedBuilding != null ? selectedBuilding.name : "unknown")); + // Notify listener that no data is available so UI can update + mainHandler.post(() -> { + if (floorDataLoadedListener != null) { + floorDataLoadedListener.onFloorDataLoaded(false); + } + }); + } + } else if (jsonResponse.startsWith("{")) { + Log.d(TAG, ">>> [API] Response is an object"); + parseResponseObject(new JSONObject(jsonResponse), expectedBuildingName); + } else { + Log.e(TAG, ">>> [API] Invalid response format: " + jsonResponse.substring(0, Math.min(100, jsonResponse.length()))); + } + + } else { + Log.e(TAG, ">>> [API] Error Code: " + responseCode); + if (responseCode == 404) { + Log.w(TAG, ">>> Building not found in server database."); + Log.w(TAG, ">>> Requested coordinates: (" + String.format("%.6f", loc.latitude) + ", " + String.format("%.6f", loc.longitude) + ")"); + } + // Try to read error response body + try { + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), StandardCharsets.UTF_8)); + StringBuilder errorSb = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) errorSb.append(line); + Log.e(TAG, ">>> [API] Error response: " + errorSb.toString()); + } catch (Exception e) { + // Ignore + } + } + conn.disconnect(); + + } catch (Exception e) { + Log.e(TAG, ">>> [API] Request Failed: " + e.getMessage()); + e.printStackTrace(); + } + }); + } + + private void parseResponseObject(JSONObject obj, String expectedBuildingName) { + try { + Log.d(TAG, ">>> [PARSE] Starting to parse response object"); + Log.d(TAG, ">>> [PARSE] Expected building: " + expectedBuildingName); + + // Extract the three required fields: name, outline, map_shapes + currentVenueName = obj.optString("name", null); + currentOutlineGeoJson = obj.optString("outline", null); + String mapShapesJsonString = obj.optString("map_shapes", null); + + Log.d(TAG, ">>> Parsing API Response:"); + Log.d(TAG, " - API Returned Venue Name: " + currentVenueName); + Log.d(TAG, " - Expected Building Name: " + (selectedBuilding != null ? selectedBuilding.name : "null")); + + if (selectedBuilding != null && currentVenueName != null) { + if (!currentVenueName.equalsIgnoreCase(selectedBuilding.name) && + !currentVenueName.contains("Nucleus") && selectedBuilding.name.contains("Library")) { + Log.w(TAG, " - WARNING: Building mismatch!"); + Log.w(TAG, " - Clicked: " + selectedBuilding.name); + Log.w(TAG, " - API returned: " + currentVenueName); + } + } + + Log.d(TAG, " - Has Outline: " + (currentOutlineGeoJson != null && !currentOutlineGeoJson.isEmpty())); + Log.d(TAG, " - Outline length: " + (currentOutlineGeoJson != null ? currentOutlineGeoJson.length() : 0) + " chars"); + Log.d(TAG, " - Has map_shapes: " + (mapShapesJsonString != null && !mapShapesJsonString.isEmpty())); + Log.d(TAG, " - map_shapes length: " + (mapShapesJsonString != null ? mapShapesJsonString.length() : 0) + " chars"); + + // Parse map_shapes: it's a JSON string containing a dictionary + // where keys are floor names and values are GeoJSON strings + floorShapesMap.clear(); + floorNamesList.clear(); + if (mapShapesJsonString != null && !mapShapesJsonString.isEmpty()) { + try { + Log.d(TAG, ">>> [PARSE] Parsing map_shapes string..."); + JSONObject shapesDict = new JSONObject(mapShapesJsonString); + Log.d(TAG, ">>> [PARSE] map_shapes has " + shapesDict.length() + " floors"); + java.util.Iterator keys = shapesDict.keys(); + int floorIndex = 0; + while (keys.hasNext()) { + String floorName = keys.next(); + String floorGeoJson = shapesDict.getString(floorName); + floorShapesMap.put(floorName, floorGeoJson); + floorNamesList.add(floorName); + Log.d(TAG, " - Found floor " + (++floorIndex) + ": " + floorName + " (" + floorGeoJson.length() + " chars)"); + } + + // Sort floors in logical order (ground, 0, 1, 2...) + floorNamesList.sort((f1, f2) -> { + int priority1 = getFloorPriority(f1); + int priority2 = getFloorPriority(f2); + return Integer.compare(priority1, priority2); + }); + currentFloor = findDefaultFloorIndex(); + + Log.d(TAG, " - Floor order after sorting: " + floorNamesList); + Log.d(TAG, " - Default floor index: " + currentFloor + " (" + getCurrentFloorName() + ")"); + } catch ( + JSONException e) { + Log.e(TAG, ">>> Failed to parse map_shapes dictionary: " + e.getMessage()); + e.printStackTrace(); + } + } else { + Log.w(TAG, ">>> [PARSE] No map_shapes data in response!"); + } + + // Keep user-selected building stable even if the API returns a nearby venue. + // We still keep currentVenueName for diagnostics and display purposes. + if (currentVenueName != null && selectedBuilding != null) { + Log.d(TAG, ">>> [PARSE] Venue match check:"); + Log.d(TAG, " - Requested: " + expectedBuildingName); + Log.d(TAG, " - API returned: " + currentVenueName); + if (!currentVenueName.equalsIgnoreCase(expectedBuildingName)) { + Log.w(TAG, " - Venue mismatch detected. Keeping selected building unchanged."); + } + } + + // Render the data on the map + mainHandler.post(() -> { + hideMap(); // Clear previous drawings + + // Draw outline first (if available) + if (currentOutlineGeoJson != null && !currentOutlineGeoJson.isEmpty()) { + Log.d(TAG, ">>> Drawing building outline"); + renderGeoJson(currentOutlineGeoJson, Color.argb(80, 0, 0, 255), 6); + + // Parse and store building boundary for collision detection + try { + parseBuildingBoundary(currentOutlineGeoJson); + } catch (Exception e) { + Log.e(TAG, "Failed to parse building boundary: " + e.getMessage()); + } + } else { + Log.w(TAG, ">>> No outline data to draw"); + } + + // Draw ground floor shapes by default (if indoor map is visible) + if (isIndoorMapVisible) { + Log.d(TAG, ">>> Rendering current floor (index " + currentFloor + ")"); + renderCurrentFloor(); + } else { + Log.d(TAG, ">>> Indoor map hidden by user, skipping floor render"); + } + + // Notify listener that floor data is ready + if (floorDataLoadedListener != null) { + boolean hasFloors = !floorNamesList.isEmpty(); + Log.d(TAG, ">>> Notifying floor data loaded listener (hasData=" + hasFloors + ")"); + floorDataLoadedListener.onFloorDataLoaded(hasFloors); + } + }); + + } catch (Exception e) { + Log.e(TAG, ">>> JSON Parse Error: " + e.getMessage()); + e.printStackTrace(); + } + } + + private void renderGeoJson(String jsonString, int color, float width) { + renderGeoJsonWithWallData(jsonString, color, width, null); + } + + // Render GeoJSON and optionally store wall data for collision detection +// Parameter jsonString: GeoJSON string to render +// Parameter color: Line color +// Parameter width: Line width +// Parameter floorName: Floor name (if not null, stores wall data for this floor) + private void renderGeoJsonWithWallData(String jsonString, int color, float width, String floorName) { + try { + JSONObject geoJson; + if (jsonString.trim().startsWith("{") && !jsonString.contains("FeatureCollection")) { + JSONObject wrapper = new JSONObject(jsonString); + String key = wrapper.keys().next(); + geoJson = wrapper.getJSONObject(key); + } else { + geoJson = new JSONObject(jsonString); + } + + JSONArray features = geoJson.optJSONArray("features"); + if (features == null) return; + + // Store wall data if floorName is provided + List> wallsForThisFloor = new ArrayList<>(); + List> stairsForThisFloor = new ArrayList<>(); + List> liftsForThisFloor = new ArrayList<>(); + + for (int i = 0; i < features.length(); i++) { + JSONObject feature = features.getJSONObject(i); + int featureKind = classifyFeatureKind(feature); + int strokeColor = getFeatureStrokeColor(featureKind, color); + int fillColor = getFeatureFillColor(featureKind); + float featureWidth = getFeatureStrokeWidth(featureKind, width); + JSONObject geometry = feature.getJSONObject("geometry"); + String type = geometry.getString("type"); + JSONArray coordinates = geometry.getJSONArray("coordinates"); + + if (type.equals("MultiPolygon")) { + for (int j = 0; j < coordinates.length(); j++) { + JSONArray polygon = coordinates.getJSONArray(j); + JSONArray ring = polygon.getJSONArray(0); + List points = parseCoordinates(ring); + + // Draw polygon + Polygon p = gMap.addPolygon(new PolygonOptions() + .addAll(points) + .strokeColor(strokeColor) + .strokeWidth(featureWidth) + .fillColor(fillColor)); + drawnShapes.add(p); + + // Only wall-like features should constrain movement. + if (floorName != null && shouldStoreAsWall(featureKind, type)) { + wallsForThisFloor.add(points); + } + if (floorName != null && featureKind == FEATURE_KIND_STAIRS) { + stairsForThisFloor.add(points); + } + if (floorName != null && featureKind == FEATURE_KIND_LIFT) { + liftsForThisFloor.add(points); + } + } + } else if (type.equals("MultiLineString")) { + for (int j = 0; j < coordinates.length(); j++) { + JSONArray line = coordinates.getJSONArray(j); + List points = parseCoordinates(line); + + // Draw polyline + com.google.android.gms.maps.model.Polyline p = gMap.addPolyline(new PolylineOptions() + .addAll(points) + .color(strokeColor) + .width(featureWidth)); + drawnShapes.add(p); + + if (floorName != null && shouldStoreAsWall(featureKind, type)) { + wallsForThisFloor.add(points); + } + if (floorName != null && featureKind == FEATURE_KIND_STAIRS) { + stairsForThisFloor.add(points); + } + if (floorName != null && featureKind == FEATURE_KIND_LIFT) { + liftsForThisFloor.add(points); + } + } + } else if (type.equals("Polygon")) { + JSONArray ring = coordinates.getJSONArray(0); + List points = parseCoordinates(ring); + + // Draw polygon + Polygon p = gMap.addPolygon(new PolygonOptions() + .addAll(points) + .strokeColor(strokeColor) + .strokeWidth(featureWidth) + .fillColor(fillColor)); + drawnShapes.add(p); + + if (floorName != null && shouldStoreAsWall(featureKind, type)) { + wallsForThisFloor.add(points); + } + if (floorName != null && featureKind == FEATURE_KIND_STAIRS) { + stairsForThisFloor.add(points); + } + if (floorName != null && featureKind == FEATURE_KIND_LIFT) { + liftsForThisFloor.add(points); + } + } else if (type.equals("LineString")) { + List points = parseCoordinates(coordinates); + + // Draw polyline + com.google.android.gms.maps.model.Polyline p = gMap.addPolyline(new PolylineOptions() + .addAll(points) + .color(strokeColor) + .width(featureWidth)); + drawnShapes.add(p); + + if (floorName != null && shouldStoreAsWall(featureKind, type)) { + wallsForThisFloor.add(points); + } + if (floorName != null && featureKind == FEATURE_KIND_STAIRS) { + stairsForThisFloor.add(points); + } + if (floorName != null && featureKind == FEATURE_KIND_LIFT) { + liftsForThisFloor.add(points); + } + } + } + + // Store wall data for this floor + if (floorName != null && !wallsForThisFloor.isEmpty()) { + floorWallsMap.put(floorName, wallsForThisFloor); + Log.d(TAG, ">>> Stored " + wallsForThisFloor.size() + " walls for floor: " + floorName); + } + if (floorName != null) { + if (!stairsForThisFloor.isEmpty()) { + floorStairsMap.put(floorName, stairsForThisFloor); + Log.d(TAG, ">>> Stored " + stairsForThisFloor.size() + " stairs zones for floor: " + floorName); + } + if (!liftsForThisFloor.isEmpty()) { + floorLiftsMap.put(floorName, liftsForThisFloor); + Log.d(TAG, ">>> Stored " + liftsForThisFloor.size() + " lift zones for floor: " + floorName); + } + } + + isIndoorMapSet = true; + Log.d(TAG, ">>> Vector shapes rendered successfully."); + + } catch (Exception e) { + Log.e(TAG, ">>> Vector Render Failed: " + e.getMessage()); + e.printStackTrace(); + } + } + + private List parseCoordinates(JSONArray ring) throws Exception { + List list = new ArrayList<>(); + for (int k = 0; k < ring.length(); k++) { + JSONArray coord = ring.getJSONArray(k); + double lon = coord.getDouble(0); + double lat = coord.getDouble(1); + list.add(new LatLng(lat, lon)); + } + return list; + } + + private int classifyFeatureKind(JSONObject feature) { + String metadata = buildFeatureMetadata(feature); + if (metadata.isEmpty()) { + return FEATURE_KIND_DEFAULT; + } + + if (metadata.contains("stair") || metadata.contains("stairs") + || metadata.contains("staircase") || metadata.contains("step")) { + return FEATURE_KIND_STAIRS; + } + + if (metadata.contains("lift") || metadata.contains("elevator")) { + return FEATURE_KIND_LIFT; + } + + if (metadata.contains("wall") || metadata.contains("partition") + || metadata.contains("boundary") || metadata.contains("outline") + || metadata.contains("obstacle")) { + return FEATURE_KIND_WALL; + } + + return FEATURE_KIND_DEFAULT; + } + + private String buildFeatureMetadata(JSONObject feature) { + StringBuilder metadata = new StringBuilder(); + JSONObject properties = feature.optJSONObject("properties"); + if (properties != null) { + appendJsonValues(properties, metadata); + } + + JSONObject geometry = feature.optJSONObject("geometry"); + if (geometry != null) { + String geometryType = geometry.optString("type", ""); + if (!geometryType.isEmpty()) { + metadata.append(' ').append(geometryType); + } + } + + return metadata.toString().toLowerCase(Locale.US); + } + + private void appendJsonValues(JSONObject object, StringBuilder metadata) { + @SuppressWarnings("unchecked") + java.util.Iterator keys = object.keys(); + while (keys.hasNext()) { + String key = keys.next(); + metadata.append(' ').append(key); + Object value = object.opt(key); + if (value instanceof JSONObject) { + appendJsonValues((JSONObject) value, metadata); + } else if (value != null) { + metadata.append(' ').append(String.valueOf(value)); + } + } + } + + private boolean shouldStoreAsWall(int featureKind, String geometryType) { + if (featureKind == FEATURE_KIND_WALL) { + return true; + } + + if (featureKind == FEATURE_KIND_STAIRS || featureKind == FEATURE_KIND_LIFT) { + return false; + } + + return "LineString".equals(geometryType) || "MultiLineString".equals(geometryType); + } + + private int getFeatureStrokeColor(int featureKind, int fallbackColor) { + switch (featureKind) { + case FEATURE_KIND_WALL: + return COLOR_FEATURE_WALL_STROKE; + case FEATURE_KIND_STAIRS: + return COLOR_FEATURE_STAIRS_STROKE; + case FEATURE_KIND_LIFT: + return COLOR_FEATURE_LIFT_STROKE; + default: + return fallbackColor == Color.BLACK ? COLOR_FEATURE_DEFAULT_STROKE : fallbackColor; + } + } + + private int getFeatureFillColor(int featureKind) { + switch (featureKind) { + case FEATURE_KIND_STAIRS: + return COLOR_FEATURE_STAIRS_FILL; + case FEATURE_KIND_LIFT: + return COLOR_FEATURE_LIFT_FILL; + case FEATURE_KIND_WALL: + return COLOR_FEATURE_WALL_FILL; + default: + return COLOR_FEATURE_DEFAULT_FILL; + } + } + + private float getFeatureStrokeWidth(int featureKind, float fallbackWidth) { + if (featureKind == FEATURE_KIND_WALL) { + return Math.max(fallbackWidth, 3.5f); + } + if (featureKind == FEATURE_KIND_STAIRS || featureKind == FEATURE_KIND_LIFT) { + return Math.max(2.4f, fallbackWidth - 0.2f); + } + return Math.max(2.0f, fallbackWidth); + } + + // Parse building boundary from outline GeoJSON for collision detection + private void parseBuildingBoundary(String outlineGeoJson) throws Exception { + buildingBoundary = null; // Clear previous boundary + + JSONObject geoJson; + if (outlineGeoJson.trim().startsWith("{") && !outlineGeoJson.contains("FeatureCollection")) { + JSONObject wrapper = new JSONObject(outlineGeoJson); + String key = wrapper.keys().next(); + geoJson = wrapper.getJSONObject(key); + } else { + geoJson = new JSONObject(outlineGeoJson); + } + + JSONArray features = geoJson.optJSONArray("features"); + if (features == null || features.length() == 0) return; + + // Get the first polygon as building boundary + JSONObject feature = features.getJSONObject(0); + JSONObject geometry = feature.getJSONObject("geometry"); + String type = geometry.getString("type"); + JSONArray coordinates = geometry.getJSONArray("coordinates"); + + if (type.equals("MultiPolygon")) { + // Take first polygon + JSONArray polygon = coordinates.getJSONArray(0); + JSONArray ring = polygon.getJSONArray(0); + buildingBoundary = parseCoordinates(ring); + } else if (type.equals("Polygon")) { + JSONArray ring = coordinates.getJSONArray(0); + buildingBoundary = parseCoordinates(ring); + } + + if (buildingBoundary != null && !buildingBoundary.isEmpty()) { + Log.d(TAG, ">>> Building boundary parsed: " + buildingBoundary.size() + " points"); + } + } + + private void downloadAndShowImage(String imageUrl) { + try { + URL url = new URL(imageUrl); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setConnectTimeout(10000); + conn.connect(); + InputStream in = conn.getInputStream(); + Bitmap bitmap = BitmapFactory.decodeStream(in); + if (bitmap != null) { + mainHandler.post(() -> updateOverlay(bitmap)); + } + conn.disconnect(); + } catch (Exception e) { + Log.e(TAG, ">>> Image Download Error: " + e.getMessage()); + } + } + + // Helper methods + + // Track selected polygon for visual feedback + private Polygon selectedPolygon = null; + private int selectedPolygonOriginalStroke = Color.DKGRAY; + + public void addFallbackBuildings() { + List fallbackList = new ArrayList<>(); + + // Nucleus building + // Precise polygon based on BuildingPolygon.java reference + List nucleusPoints = new ArrayList<>(); + nucleusPoints.add(new LatLng(55.92332, -3.17388)); // NE corner + nucleusPoints.add(new LatLng(55.92282, -3.17388)); // SE corner + nucleusPoints.add(new LatLng(55.92282, -3.17460)); // SW corner + nucleusPoints.add(new LatLng(55.92332, -3.17460)); // NW corner + fallbackList.add(new IndoorBuilding("venue_nucleus", "The Nucleus Building", nucleusPoints, calculateBounds(nucleusPoints), new HashMap<>(), 4.2f, NUCLEUS_FLOOR_ALTITUDE_ANCHORS)); + + // Murray Library + // Moved west to eliminate overlap with Nucleus (east edge at -3.17477) + List libraryPoints = new ArrayList<>(); + libraryPoints.add(new LatLng(55.92307, -3.17477)); // NE corner + libraryPoints.add(new LatLng(55.92281, -3.17477)); // SE corner + libraryPoints.add(new LatLng(55.92281, -3.17518)); // SW corner + libraryPoints.add(new LatLng(55.92307, -3.17518)); // NW corner + fallbackList.add(new IndoorBuilding("venue_library", "Murray Library", libraryPoints, calculateBounds(libraryPoints), new HashMap<>(), 4.0f)); + + // Murchison House + List murchisonPoints = new ArrayList<>(); + murchisonPoints.add(new LatLng(55.92447, -3.17868)); // NE + murchisonPoints.add(new LatLng(55.92379, -3.17868)); // SE + murchisonPoints.add(new LatLng(55.92379, -3.17964)); // SW + murchisonPoints.add(new LatLng(55.92447, -3.17964)); // NW + fallbackList.add(new IndoorBuilding("venue_murchison", "Murchison House", murchisonPoints, calculateBounds(murchisonPoints), new HashMap<>(), 4.0f)); + + // Fleeming Jenkin Building + List fjbPoints = new ArrayList<>(); + fjbPoints.add(new LatLng(55.92282, -3.17259)); // NE + fjbPoints.add(new LatLng(55.92221, -3.17192)); // SE + fjbPoints.add(new LatLng(55.92211, -3.17228)); // SW + fjbPoints.add(new LatLng(55.92269, -3.17296)); // NW + fallbackList.add(new IndoorBuilding("venue_fjb", "Fleeming Jenkin Building", fjbPoints, calculateBounds(fjbPoints), new HashMap<>(), 3.5f)); + + Log.d(TAG, ">>> Loaded " + fallbackList.size() + " fallback buildings:"); + for (IndoorBuilding b : fallbackList) { + LatLng center = b.bounds.getCenter(); + Log.d(TAG, " - " + b.name + " at (" + String.format("%.6f", center.latitude) + ", " + String.format("%.6f", center.longitude) + ")"); + } + + fallbackBuildings.clear(); + fallbackBuildings.addAll(fallbackList); + + mainHandler.post(() -> drawBuildingOutlines(fallbackList)); + } + + private void drawBuildingOutlines(List buildings) { + // Distinct colors for each building + int[][] buildingColors = { + {Color.rgb(255, 191, 0), 0x30FFD700}, // Nucleus: gold + {Color.rgb(41, 128, 185), 0x252980B9}, // Library: blue + {Color.rgb(39, 174, 96), 0x2527AE60}, // Murchison: green + {Color.rgb(192, 57, 43), 0x25C0392B}, // FJB: red + }; + + for (int i = 0; i < buildings.size(); i++) { + IndoorBuilding b = buildings.get(i); + int strokeColor = (i < buildingColors.length) ? buildingColors[i][0] : Color.DKGRAY; + int fillColor = (i < buildingColors.length) ? buildingColors[i][1] : 0x20444444; + + Polygon poly = gMap.addPolygon(new PolygonOptions() + .addAll(b.polygonPoints) + .strokeColor(strokeColor) + .strokeWidth(4f) + .fillColor(fillColor) + .clickable(true) + .zIndex(10)); + poly.setTag(b.name); // Store building name as tag + polygonMap.put(poly, b); + } + } + + public boolean onPolygonClick(Polygon polygon) { + IndoorBuilding b = polygonMap.get(polygon); + if (b != null) { + // Reset previously selected polygon + if (selectedPolygon != null && selectedPolygon != polygon) { + selectedPolygon.setStrokeColor(selectedPolygonOriginalStroke); + selectedPolygon.setStrokeWidth(4f); + } + + selectedBuilding = b; + currentFloor = 0; + + // Clear old floor data immediately when selecting a new building + // This prevents stale floor data from a previous building being used + floorShapesMap.clear(); + floorNamesList.clear(); + floorWallsMap.clear(); + floorStairsMap.clear(); + floorLiftsMap.clear(); + buildingBoundary = null; + currentVenueName = null; + currentOutlineGeoJson = null; + hideMap(); // Clear previously drawn indoor shapes + + LatLng center = b.bounds.getCenter(); + Log.d(TAG, "===================================="); + Log.d(TAG, ">>> Building Clicked: " + b.name); + Log.d(TAG, ">>> Building ID: " + b.id); + Log.d(TAG, ">>> Center: (" + String.format("%.6f", center.latitude) + ", " + String.format("%.6f", center.longitude) + ")"); + Log.d(TAG, ">>> Requesting API for THIS building's floor data..."); + + // Highlight selected building + selectedPolygonOriginalStroke = polygon.getStrokeColor(); + selectedPolygon = polygon; + polygon.setStrokeColor(Color.rgb(0, 230, 118)); // Material green accent + polygon.setStrokeWidth(6f); + + // Persist campaign only when mapping is valid. + persistCampaignForBuildingName(b.name); + + fetchFloorPlan(center, new ArrayList<>()); + return true; + } else { + Log.w(TAG, ">>> Polygon clicked but not found in polygonMap!"); + } + return false; + } + + public boolean selectBuildingForLocation(LatLng location, boolean fetchData) { + if (location == null || fallbackBuildings.isEmpty()) { + return false; + } + + IndoorBuilding bestMatch = null; + double bestDistanceMeters = Double.MAX_VALUE; + + for (IndoorBuilding building : fallbackBuildings) { + boolean insideBuilding = GeometryUtils.isPointInPolygon(location, building.polygonPoints); + double distanceMeters = distanceMeters(location, building.bounds.getCenter()); + + if (insideBuilding) { + bestMatch = building; + bestDistanceMeters = 0.0; + break; + } + + if (distanceMeters < bestDistanceMeters) { + bestDistanceMeters = distanceMeters; + bestMatch = building; + } + } + + if (bestMatch == null || bestDistanceMeters > 80.0) { + return false; + } + + applyBuildingSelection(bestMatch, fetchData); + return true; + } + + private void applyBuildingSelection(IndoorBuilding building, boolean fetchData) { + selectedBuilding = building; + currentFloor = 0; + + floorShapesMap.clear(); + floorNamesList.clear(); + floorWallsMap.clear(); + floorStairsMap.clear(); + floorLiftsMap.clear(); + buildingBoundary = null; + currentVenueName = null; + currentOutlineGeoJson = null; + hideMap(); + + highlightSelectedBuilding(building); + + persistCampaignForBuildingName(building.name); + + if (fetchData) { + fetchFloorPlan(building.bounds.getCenter(), new ArrayList<>()); + } + } + + private void highlightSelectedBuilding(IndoorBuilding building) { + if (selectedPolygon != null) { + selectedPolygon.setStrokeColor(selectedPolygonOriginalStroke); + selectedPolygon.setStrokeWidth(4f); + selectedPolygon = null; + } + + for (Map.Entry entry : polygonMap.entrySet()) { + IndoorBuilding candidate = entry.getValue(); + if (candidate != null && building != null && candidate.name.equals(building.name)) { + Polygon polygon = entry.getKey(); + selectedPolygonOriginalStroke = polygon.getStrokeColor(); + selectedPolygon = polygon; + polygon.setStrokeColor(Color.rgb(0, 230, 118)); + polygon.setStrokeWidth(6f); + break; + } + } + } + + private double distanceMeters(LatLng a, LatLng b) { + double lat1 = Math.toRadians(a.latitude); + double lon1 = Math.toRadians(a.longitude); + double lat2 = Math.toRadians(b.latitude); + double lon2 = Math.toRadians(b.longitude); + + double dLat = lat2 - lat1; + double dLon = lon2 - lon1; + double haversine = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) * Math.sin(dLon / 2); + return 6371000.0 * 2.0 * Math.atan2(Math.sqrt(haversine), Math.sqrt(1.0 - haversine)); + } + + public void hideMap() { + if (groundOverlay != null) { + groundOverlay.remove(); + groundOverlay = null; + } + // Clear drawn vector shapes + for (Object shape : drawnShapes) { + if (shape instanceof Polygon) ((Polygon) shape).remove(); + if (shape instanceof com.google.android.gms.maps.model.Polyline) ((com.google.android.gms.maps.model.Polyline) shape).remove(); + } + drawnShapes.clear(); + isIndoorMapSet = false; + } + + private void updateOverlay(Bitmap bitmap) { + if (bitmap == null || selectedBuilding == null) return; + hideMap(); + + try { + groundOverlay = gMap.addGroundOverlay(new GroundOverlayOptions() + .image(BitmapDescriptorFactory.fromBitmap(bitmap)) + .positionFromBounds(selectedBuilding.bounds) + .zIndex(100) + .transparency(0.1f)); + isIndoorMapSet = true; + } catch (Exception e) { + Log.e(TAG, "Overlay Error: " + e.getMessage()); + } + } + + // Setter methods + public void setCurrentLocation(LatLng loc) { this.currentLocation = loc; } + public void fetchBuildingsFromApi(LatLng loc) { fetchFloorPlan(loc, new ArrayList<>()); } + public void setOnFloorDataLoadedListener(OnFloorDataLoadedListener listener) { + this.floorDataLoadedListener = listener; + } + + // Set selected building before API call (for validation) + // This is critical for building name verification in API responses + public void setSelectedBuilding(String name, LatLng center) { + IndoorBuilding fallbackMatch = findFallbackBuildingByName(name); + List polygonPoints = new ArrayList<>(); + LatLngBounds bounds = new LatLngBounds(center, center); + float floorHeight = 4.0f; + float[] floorAltitudeAnchorsMeters = null; + + if (fallbackMatch != null) { + polygonPoints.addAll(fallbackMatch.polygonPoints); + bounds = fallbackMatch.bounds; + floorHeight = fallbackMatch.floorHeight; + if (fallbackMatch.floorAltitudeAnchorsMeters != null) { + floorAltitudeAnchorsMeters = fallbackMatch.floorAltitudeAnchorsMeters.clone(); + } + } else { + polygonPoints.add(center); + } + + // Clear old floor data when switching buildings + floorShapesMap.clear(); + floorNamesList.clear(); + floorWallsMap.clear(); + floorStairsMap.clear(); + floorLiftsMap.clear(); + buildingBoundary = null; + currentVenueName = null; + currentOutlineGeoJson = null; + currentFloor = 0; + hideMap(); + + selectedBuilding = new IndoorBuilding( + "venue_" + name.toLowerCase().replace(" ", "_"), + name, + polygonPoints, + bounds, + new HashMap<>(), + floorHeight, + floorAltitudeAnchorsMeters + ); + + Log.d(TAG, "===================================="); + Log.d(TAG, ">>> Selected building set: " + name); + Log.d(TAG, ">>> Building ID: " + selectedBuilding.id); + Log.d(TAG, ">>> Center: (" + String.format("%.6f", center.latitude) + ", " + String.format("%.6f", center.longitude) + ")"); + Log.d(TAG, ">>> Floor height: " + floorHeight + "m, anchors: " + (floorAltitudeAnchorsMeters != null ? floorAltitudeAnchorsMeters.length : 0)); + Log.d(TAG, ">>> This will be used to validate API response"); + } + + private int findDefaultFloorIndex() { + if (floorNamesList.isEmpty()) { + return 0; + } + + for (int i = 0; i < floorNamesList.size(); i++) { + if ("GF".equals(normalizeFloorName(floorNamesList.get(i)))) { + return i; + } + } + + return 0; + } + + private Float resolveAltitudeAnchorForFloorName(String floorName, float[] rawAnchors) { + if (rawAnchors == null || rawAnchors.length == 0) { + return null; + } + + if (isNucleusBuildingSelected() && rawAnchors.length >= 5) { + switch (normalizeFloorName(floorName)) { + case "B1": + return rawAnchors[0]; + case "GF": + return rawAnchors[1]; + case "F1": + return rawAnchors[2]; + case "F2": + return rawAnchors[3]; + case "F3": + return rawAnchors[4]; + default: + return null; + } + } + + return null; + } + + private boolean isNucleusBuildingSelected() { + String buildingName = getSelectedBuildingName(); + return buildingName != null && buildingName.toLowerCase(Locale.US).contains("nucleus"); + } + + private IndoorBuilding findFallbackBuildingByName(String name) { + if (name == null) { + return null; + } + + String lowerName = name.toLowerCase(Locale.US); + for (IndoorBuilding building : fallbackBuildings) { + String lowerBuilding = building.name.toLowerCase(Locale.US); + if ((lowerName.contains("nucleus") && lowerBuilding.contains("nucleus")) + || (lowerName.contains("library") && lowerBuilding.contains("library")) + || (lowerName.contains("murchison") && lowerBuilding.contains("murchison")) + || ((lowerName.contains("fjb") || lowerName.contains("fleeming") || lowerName.contains("jenkin")) + && (lowerBuilding.contains("fjb") || lowerBuilding.contains("fleeming") || lowerBuilding.contains("jenkin")))) { + return building; + } + } + + return null; + } + + private String normalizeFloorName(String floorName) { + if (floorName == null) { + return ""; + } + + String lower = floorName.trim().toLowerCase(Locale.US); + String compact = lower.replace(" ", "").replace("_", "").replace("-", ""); + if (compact.equals("g") || compact.equals("gf") || compact.equals("ground") + || compact.equals("groundfloor") || compact.equals("0") + || compact.equals("level0") || compact.equals("l0") || compact.equals("floor0")) { + return "GF"; + } + + Integer extracted = extractFloorNumber(lower); + if (lower.contains("basement") || compact.startsWith("b")) { + int basementLevel = extracted != null ? Math.max(1, Math.abs(extracted)) : 1; + return "B" + basementLevel; + } + + if (extracted != null) { + if (extracted == 0) { + return "GF"; + } + if (extracted < 0) { + return "B" + Math.abs(extracted); + } + return "F" + extracted; + } + + return compact.toUpperCase(Locale.US); + } + + private Integer extractFloorNumber(String value) { + Matcher matcher = FLOOR_NUMBER_PATTERN.matcher(value); + if (!matcher.find()) { + return null; + } + try { - // Setting overlay if in Nucleus and not already set - if (BuildingPolygon.inNucleus(currentLocation) && !isIndoorMapSet) { - groundOverlay = gMap.addGroundOverlay(new GroundOverlayOptions() - .image(BitmapDescriptorFactory.fromResource(R.drawable.nucleusg)) - .positionFromBounds(NUCLEUS)); - isIndoorMapSet = true; - // Nucleus has an LG floor so G floor is at index 1 - currentFloor=1; - floorHeight=NUCLEUS_FLOOR_HEIGHT; - } - // Setting overlay if in Library and not already set - else if (BuildingPolygon.inLibrary(currentLocation) && !isIndoorMapSet) { - groundOverlay = gMap.addGroundOverlay(new GroundOverlayOptions() - .image(BitmapDescriptorFactory.fromResource(R.drawable.libraryg)) - .positionFromBounds(LIBRARY)); - isIndoorMapSet = true; - currentFloor=0; - floorHeight=LIBRARY_FLOOR_HEIGHT; - } - // Removing overlay if user no longer in area with indoor maps available - else if (!BuildingPolygon.inLibrary(currentLocation) && - !BuildingPolygon.inNucleus(currentLocation)&& isIndoorMapSet){ - groundOverlay.remove(); - isIndoorMapSet = false; - currentFloor=0; - } - } catch (Exception ex) { - Log.e("Error with overlay, Exception:", ex.toString()); - } - } - - /** - * Function used to set the indication of available floor maps for building using green Polylines - * along the building's boundaries. - */ - public void setIndicationOfIndoorMap(){ - //Indicator for Nucleus Building - List points=BuildingPolygon.NUCLEUS_POLYGON; - // Closing Boundary - points.add(BuildingPolygon.NUCLEUS_POLYGON.get(0)); - gMap.addPolyline(new PolylineOptions().color(Color.GREEN) - .addAll(points)); - - // Indicator for the Library Building - points=BuildingPolygon.LIBRARY_POLYGON; - // Closing Boundary - points.add(BuildingPolygon.LIBRARY_POLYGON.get(0)); - gMap.addPolyline(new PolylineOptions().color(Color.GREEN) - .addAll(points)); + return Integer.parseInt(matcher.group()); + } catch (NumberFormatException e) { + return null; + } + } + + // Toggle indoor map visibility (toggle floor plan display) + public void setIndoorMapVisible(boolean visible) { + this.isIndoorMapVisible = visible; + if (!visible) { + // Hide indoor shapes but keep building outlines + List shapesToRemove = new ArrayList<>(); + for (Object shape : drawnShapes) { + if (shape instanceof com.google.android.gms.maps.model.Polyline) { + ((com.google.android.gms.maps.model.Polyline) shape).setVisible(false); + } else if (shape instanceof Polygon) { + ((Polygon) shape).setVisible(false); + } + } + Log.d(TAG, ">>> Indoor map hidden"); + } else { + // Show indoor shapes + for (Object shape : drawnShapes) { + if (shape instanceof com.google.android.gms.maps.model.Polyline) { + ((com.google.android.gms.maps.model.Polyline) shape).setVisible(true); + } else if (shape instanceof Polygon) { + ((Polygon) shape).setVisible(true); + } + } + Log.d(TAG, ">>> Indoor map shown"); + } + } + + public boolean isIndoorMapVisible() { return isIndoorMapVisible; } + public String getSelectedVenueId() { return selectedBuilding != null ? selectedBuilding.name : "None"; } + public String getSelectedBuildingId() { return selectedBuilding != null ? selectedBuilding.id : null; } + public String getSelectedBuildingName() { return selectedBuilding != null ? selectedBuilding.name : null; } + public float getFloorHeight() { return selectedBuilding != null ? selectedBuilding.floorHeight : 4.0f; } + public List> getCurrentFloorStairsZones() { + String currentFloorName = getCurrentFloorName(); + if (currentFloorName == null || !floorStairsMap.containsKey(currentFloorName)) { + return new ArrayList<>(); + } + return new ArrayList<>(floorStairsMap.get(currentFloorName)); + } + public List> getCurrentFloorLiftZones() { + String currentFloorName = getCurrentFloorName(); + if (currentFloorName == null || !floorLiftsMap.containsKey(currentFloorName)) { + return new ArrayList<>(); + } + return new ArrayList<>(floorLiftsMap.get(currentFloorName)); + } + public boolean isNearCurrentFloorStairs(LatLng location) { + return isNearFeatureCollection(location, getCurrentFloorStairsZones()); + } + public boolean isNearCurrentFloorLift(LatLng location) { + return isNearFeatureCollection(location, getCurrentFloorLiftZones()); + } + public boolean isNearCurrentFloorVerticalTransition(LatLng location) { + return isNearCurrentFloorStairs(location) || isNearCurrentFloorLift(location); + } + public float[] getFloorAltitudeAnchors() { + if (selectedBuilding == null || selectedBuilding.floorAltitudeAnchorsMeters == null) { + return null; + } + + float[] rawAnchors = selectedBuilding.floorAltitudeAnchorsMeters; + if (floorNamesList.isEmpty()) { + return rawAnchors.clone(); + } + + float[] alignedAnchors = new float[floorNamesList.size()]; + boolean allResolved = true; + for (int i = 0; i < floorNamesList.size(); i++) { + Float anchor = resolveAltitudeAnchorForFloorName(floorNamesList.get(i), rawAnchors); + if (anchor == null) { + allResolved = false; + break; + } + alignedAnchors[i] = anchor; + } + + return allResolved ? alignedAnchors : rawAnchors.clone(); + } + public int getCurrentFloor() { return currentFloor; } + public int getAvailableFloorsCount() { return floorNamesList.size(); } + + // Map SensorFusion barometer band floor index to current building floor index. + // SensorFusion uses: 0=B1, 1=GF, 2=F1, 3=F2, 4=F3+. + public int mapBarometerBandFloorToFloorIndex(int barometerBandFloor) { + if (floorNamesList.isEmpty()) { + return barometerBandFloor; + } + + int targetPriority = barometerBandFloor - 1; // B1=-1, GF=0, F1=1, ... + int bestIndex = 0; + int bestDistance = Integer.MAX_VALUE; + + for (int i = 0; i < floorNamesList.size(); i++) { + int priority = getFloorPriority(floorNamesList.get(i)); + if (priority == 999) { + continue; + } + + int distance = Math.abs(priority - targetPriority); + if (distance < bestDistance) { + bestDistance = distance; + bestIndex = i; + } + } + + return bestIndex; + } + + // Get the current floor name as it appears in the map data + public String getCurrentFloorName() { + if (floorNamesList.isEmpty()) return null; + + // Get floor name by index from ordered list + if (currentFloor >= 0 && currentFloor < floorNamesList.size()) { + return floorNamesList.get(currentFloor); + } + + return null; + } + public void setCurrentFloor(int floor, boolean auto) { + if (!floorNamesList.isEmpty()) { + floor = Math.max(0, Math.min(floor, floorNamesList.size() - 1)); + } + this.currentFloor = floor; + if (selectedBuilding != null && !floorShapesMap.isEmpty()) { + // Don't re-fetch from API, just re-render from cached data + mainHandler.post(() -> { + // Clear indoor shapes (both Polygons and Polylines), keep the outline + List shapesToRemove = new ArrayList<>(); + for (Object shape : drawnShapes) { + if (shape instanceof com.google.android.gms.maps.model.Polyline) { + ((com.google.android.gms.maps.model.Polyline) shape).remove(); + shapesToRemove.add(shape); + } else if (shape instanceof Polygon) { + // Only remove if it's not the building outline + Polygon polygon = (Polygon) shape; + // Building outlines have specific color (blue), indoor shapes are black + if (polygon.getStrokeColor() != Color.argb(80, 0, 0, 255) && + polygon.getStrokeColor() != Color.GREEN) { + polygon.remove(); + shapesToRemove.add(shape); + } + } + } + drawnShapes.removeAll(shapesToRemove); + Log.d(TAG, ">>> Cleared " + shapesToRemove.size() + " indoor shapes for floor switch"); + if (isIndoorMapVisible) { + renderCurrentFloor(); + } + }); + } + } + + // Helper method to determine floor priority for sorting + private int getFloorPriority(String floorName) { + String normalized = normalizeFloorName(floorName); + if ("GF".equals(normalized)) { + return 0; + } + + if (normalized.startsWith("B")) { + try { + return -Integer.parseInt(normalized.substring(1)); + } catch (NumberFormatException e) { + return 999; + } + } + + if (normalized.startsWith("F")) { + try { + return Integer.parseInt(normalized.substring(1)); + } catch (NumberFormatException e) { + return 999; + } + } + + return 999; + } + + // Render the current floor's shapes from cached data. + // Uses the ordered floor list to access floors by index. + private void renderCurrentFloor() { + if (floorNamesList.isEmpty() || floorShapesMap.isEmpty()) { + Log.d(TAG, ">>> No floor data available to render"); + return; + } + + // Get floor name by index from ordered list + if (currentFloor >= 0 && currentFloor < floorNamesList.size()) { + String floorName = floorNamesList.get(currentFloor); + String floorGeoJson = floorShapesMap.get(floorName); + + if (floorGeoJson != null) { + Log.d(TAG, ">>> Rendering floor: " + floorName + " (index=" + currentFloor + " of " + floorNamesList.size() + ")"); + + // Clear existing wall data for this floor before re-rendering + floorWallsMap.remove(floorName); + floorStairsMap.remove(floorName); + floorLiftsMap.remove(floorName); + + // Render and store wall data for collision detection + renderGeoJsonWithWallData(floorGeoJson, Color.BLACK, 3, floorName); + isIndoorMapSet = true; + } else { + Log.w(TAG, ">>> Floor data missing for: " + floorName); + } + } else { + Log.w(TAG, ">>> Invalid floor index: " + currentFloor + " (available: 0-" + (floorNamesList.size() - 1) + ")"); + } + } + + public void increaseFloor() { + if (!floorNamesList.isEmpty()) { + int maxFloor = floorNamesList.size() - 1; + if (currentFloor < maxFloor) { + setCurrentFloor(currentFloor + 1, false); + String floorName = floorNamesList.get(currentFloor); + Log.d(TAG, "Increased to floor: " + floorName + " (index=" + currentFloor + ")"); + } else { + Log.d(TAG, "Already at top floor: " + currentFloor + "/" + maxFloor); + } + } + } + + public void decreaseFloor() { + if (!floorNamesList.isEmpty()) { + if (currentFloor > 0) { + setCurrentFloor(currentFloor - 1, false); + String floorName = floorNamesList.get(currentFloor); + Log.d(TAG, "Decreased to floor: " + floorName + " (index=" + currentFloor + ")"); + } else { + Log.d(TAG, "Already at ground floor: 0"); + } + } + } + + private LatLngBounds calculateBounds(List points) { + LatLngBounds.Builder b = new LatLngBounds.Builder(); + for (LatLng p : points) b.include(p); + return b.build(); + } + + // Check if indoor constraints (walls, boundaries) are available for current floor + public boolean hasIndoorConstraints() { + // Check if we have wall data for the current floor and indoor map is visible + if (!isIndoorMapVisible) { + return false; // Don't apply constraints when indoor map is hidden + } + + String currentFloorName = getCurrentFloorName(); + if (currentFloorName != null && floorWallsMap.containsKey(currentFloorName)) { + List> walls = floorWallsMap.get(currentFloorName); + return walls != null && !walls.isEmpty(); + } + + // Also check if we have building boundary + return buildingBoundary != null && !buildingBoundary.isEmpty(); + } + + private boolean isNearFeatureCollection(LatLng location, List> features) { + if (location == null || features == null || features.isEmpty()) { + return false; + } + + for (List feature : features) { + if (GeometryUtils.isPointNearFeature(location, feature, 8.0)) { + return true; + } + } + + return false; + } + + // Validate a position against indoor constraints (wall collision, boundary check) + // Prevents the position marker from going through walls or outside building + // Enhanced with wall sliding - allows movement parallel to walls + public LatLng validatePosition(LatLng newLoc, LatLng oldLoc) { + if (newLoc == null) return oldLoc; + if (oldLoc == null) return newLoc; // First position, no validation needed + if (!isIndoorMapVisible) return newLoc; // No constraints when indoor map is off + + // Check building boundary + if (buildingBoundary != null && !buildingBoundary.isEmpty()) { + if (!GeometryUtils.isPointInPolygon(newLoc, buildingBoundary)) { + // New position is outside building - constrain to boundary + Log.d(TAG, "Position outside building boundary - constraining"); + return GeometryUtils.constrainToPolygon(newLoc, buildingBoundary); + } + } + + // Check wall collision for current floor + String currentFloorName = getCurrentFloorName(); + if (currentFloorName != null && floorWallsMap.containsKey(currentFloorName)) { + List> walls = floorWallsMap.get(currentFloorName); + if (walls != null && !walls.isEmpty()) { + // Check if movement from oldLoc to newLoc crosses any wall + if (GeometryUtils.crossesWall(oldLoc, newLoc, walls)) { + Log.d(TAG, "Wall collision detected - attempting wall slide"); + + // Try to slide along the wall instead of stopping completely + // Decompose movement into x and y components + LatLng slidPos = tryWallSlide(oldLoc, newLoc, walls); + if (slidPos != null && !slidPos.equals(oldLoc)) { + Log.d(TAG, "Wall slide successful - allowing partial movement"); + return slidPos; + } + + // If slide failed, stay at old position + return oldLoc; + } + } + } + + // No collision - allow movement + return newLoc; + } + + // Try to slide along a wall when direct movement is blocked + // Attempts horizontal and vertical components separately + private LatLng tryWallSlide(LatLng from, LatLng to, List> walls) { + if (from == null || to == null) { + return null; + } + + final double dLat = to.latitude - from.latitude; + final double dLon = to.longitude - from.longitude; + + List candidates = new ArrayList<>(); + + // Axis-aligned slides (works well for corridor walls). + candidates.add(new LatLng(from.latitude, to.longitude)); + candidates.add(new LatLng(to.latitude, from.longitude)); + + // Partial progress on direct vector. + double[] directRatios = new double[]{0.90, 0.75, 0.60, 0.45, 0.30, 0.15}; + for (double r : directRatios) { + candidates.add(new LatLng(from.latitude + dLat * r, from.longitude + dLon * r)); + } + + // Corner-friendly mixed moves: progress one axis more than the other. + double[] mixedRatios = new double[]{0.85, 0.65, 0.45, 0.25}; + for (double r : mixedRatios) { + candidates.add(new LatLng(from.latitude + dLat * r, from.longitude + dLon * 0.5)); + candidates.add(new LatLng(from.latitude + dLat * 0.5, from.longitude + dLon * r)); + } + + LatLng best = null; + double bestDistanceToTarget = Double.MAX_VALUE; + + for (LatLng candidate : candidates) { + if (!isSlideCandidateValid(from, candidate, walls)) { + continue; + } + + double progress = GeometryUtils.distanceBetween(from, candidate); + if (progress < 0.08) { + // Ignore near-zero movement to avoid sticking in place. + continue; + } + + double distanceToTarget = GeometryUtils.distanceBetween(candidate, to); + if (distanceToTarget < bestDistanceToTarget) { + bestDistanceToTarget = distanceToTarget; + best = candidate; + } + } + + return best; + } + + private boolean isSlideCandidateValid(LatLng from, LatLng candidate, List> walls) { + if (candidate == null || from == null) { + return false; + } + if (GeometryUtils.crossesWall(from, candidate, walls)) { + return false; + } + if (buildingBoundary != null && !buildingBoundary.isEmpty() + && !GeometryUtils.isPointInPolygon(candidate, buildingBoundary)) { + return false; + } + return true; + } + + + private String convertNameToApiId(String displayName) { + if (displayName == null) { + return ""; + } + + String lowerName = displayName.toLowerCase().trim(); + + if (lowerName.contains("murchison")) { + return "murchison_house"; + } + if (lowerName.contains("nucleus")) { + return "nucleus_building"; + } + if (lowerName.contains("library") || lowerName.contains("murray")) { + return "library"; + } + if (lowerName.contains("fjb") || lowerName.contains("faraday")) { + return "fjb"; + } + + return ""; + } + + private void persistCampaignForBuildingName(String buildingName) { + String apiCampaignId = convertNameToApiId(buildingName); + if (apiCampaignId == null || apiCampaignId.isEmpty()) { + String previousCampaign = settings.getString("current_campaign", ""); + Log.w(TAG, "No campaign mapping for building: " + buildingName); + Log.w(TAG, "Keeping previous campaign in prefs: " + previousCampaign); + return; + } + settings.edit().putString("current_campaign", apiCampaignId).apply(); + Log.d(TAG, "Saved campaign to prefs: " + apiCampaignId); + } + + // Check if a location is inside the currently selected building's boundary +// Parameter location: The location to check +// Returns: true if location is inside the selected building, false otherwise + public boolean isLocationInsideSelectedBuilding(LatLng location) { + if (location == null) return false; + + // Check against building boundary if available + if (buildingBoundary != null && !buildingBoundary.isEmpty()) { + return GeometryUtils.isPointInPolygon(location, buildingBoundary); + } + + // Fallback: check against selected building's polygon bounds + if (selectedBuilding != null && selectedBuilding.polygonPoints != null) { + return GeometryUtils.isPointInPolygon(location, selectedBuilding.polygonPoints); + } + + return false; + } + + public boolean isLocationInsideAnyKnownBuilding(LatLng location) { + if (location == null) { + return false; + } + + if (isLocationInsideSelectedBuilding(location)) { + return true; + } + + if (!fallbackBuildings.isEmpty()) { + for (IndoorBuilding building : fallbackBuildings) { + if (building != null + && building.polygonPoints != null + && GeometryUtils.isPointInPolygon(location, building.polygonPoints)) { + return true; + } + } + } + + return BuildingPolygon.inAnyKnownBuilding(location); + } + + // Get the walls for a specific floor (or current top by default). +// Returns: List of Polylines (Lists of LatLng) representing the walls on the current floor. + public List> getCurrentFloorWalls() { + String currentFloorName = getCurrentFloorName(); + if (currentFloorName != null && floorWallsMap != null && floorWallsMap.containsKey(currentFloorName)) { + return floorWallsMap.get(currentFloorName); + } + return new ArrayList<>(); } } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/MadgwickAHRS.java b/app/src/main/java/com/openpositioning/PositionMe/utils/MadgwickAHRS.java new file mode 100644 index 00000000..a8ee6678 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/MadgwickAHRS.java @@ -0,0 +1,256 @@ +package com.openpositioning.PositionMe.utils; + +import android.util.Log; + +// Lightweight Madgwick AHRS filter for Android sensor fusion. +// Uses gyroscope integration corrected by accelerometer and magnetometer observations. +// Includes magnetometer anomaly handling for indoor interference. +public class MadgwickAHRS { + + // Default beta tuned for practical drift correction on handheld devices. + private static final float DEFAULT_BETA = 0.15f; + + + private static final float MAGNETOMETER_MIN = 20.0f; + private static final float MAGNETOMETER_MAX = 60.0f; + + private float beta; + private float q0 = 1.0f; + private float q1 = 0.0f; + private float q2 = 0.0f; + private float q3 = 0.0f; + + // Tracks consecutive anomaly events for diagnostics. + private int magneticAnomalyCount = 0; + + public MadgwickAHRS() { + this(DEFAULT_BETA); + } + + public MadgwickAHRS(float beta) { + this.beta = beta; + } + + public void setBeta(float beta) { + this.beta = beta; + } + + public void reset() { + q0 = 1.0f; + q1 = 0.0f; + q2 = 0.0f; + q3 = 0.0f; + magneticAnomalyCount = 0; + } + + public void update(float gx, float gy, float gz, + float ax, float ay, float az, + float mx, float my, float mz, + float deltaTimeSeconds) { + if (deltaTimeSeconds <= 0.0f) { + return; + } + + float accNorm = invSqrt(ax * ax + ay * ay + az * az); + if (Float.isNaN(accNorm) || Float.isInfinite(accNorm)) { + integrateGyroscopeOnly(gx, gy, gz, deltaTimeSeconds); + return; + } + ax *= accNorm; + ay *= accNorm; + az *= accNorm; + + // Detect magnetic field outliers before full fusion. + float magIntensity = (float) Math.sqrt(mx * mx + my * my + mz * mz); + + // Check if magnetic field is outside normal range (indicates indoor metal/electrical interference) + if (magIntensity < MAGNETOMETER_MIN || magIntensity > MAGNETOMETER_MAX) { + // Fall back to gyro + accelerometer when magnetometer is unreliable. + magneticAnomalyCount++; + Log.w("MadgwickAHRS", String.format("Magnetic anomaly #%d: %.1f uT (outside [%.0f-%.0f])", + magneticAnomalyCount, magIntensity, MAGNETOMETER_MIN, MAGNETOMETER_MAX)); + integrateGyroAndAccelOnly(gx, gy, gz, ax, ay, az, deltaTimeSeconds); + return; + } + + // Magnetometer is reliable; continue with full AHRS update. + magneticAnomalyCount = 0; + float magNorm = invSqrt(mx * mx + my * my + mz * mz); + + float q0q0 = q0 * q0; + float q0q1 = q0 * q1; + float q0q2 = q0 * q2; + float q0q3 = q0 * q3; + float q1q1 = q1 * q1; + float q1q2 = q1 * q2; + float q1q3 = q1 * q3; + float q2q2 = q2 * q2; + float q2q3 = q2 * q3; + float q3q3 = q3 * q3; + + float hx = 2.0f * mx * (0.5f - q2q2 - q3q3) + + 2.0f * my * (q1q2 - q0q3) + + 2.0f * mz * (q1q3 + q0q2); + float hy = 2.0f * mx * (q1q2 + q0q3) + + 2.0f * my * (0.5f - q1q1 - q3q3) + + 2.0f * mz * (q2q3 - q0q1); + float twoBx = (float) Math.sqrt(hx * hx + hy * hy); + float twoBz = 2.0f * mx * (q1q3 - q0q2) + + 2.0f * my * (q2q3 + q0q1) + + 2.0f * mz * (0.5f - q1q1 - q2q2); + float fourBx = 2.0f * twoBx; + float fourBz = 2.0f * twoBz; + + float s0 = -2.0f * q2 * (2.0f * q1q3 - 2.0f * q0q2 - ax) + + 2.0f * q1 * (2.0f * q0q1 + 2.0f * q2q3 - ay) + - twoBz * q2 * (twoBx * (0.5f - q2q2 - q3q3) + twoBz * (q1q3 - q0q2) - mx) + + (-twoBx * q3 + twoBz * q1) * (twoBx * (q1q2 - q0q3) + twoBz * (q0q1 + q2q3) - my) + + twoBx * q2 * (twoBx * (q0q2 + q1q3) + twoBz * (0.5f - q1q1 - q2q2) - mz); + float s1 = 2.0f * q3 * (2.0f * q1q3 - 2.0f * q0q2 - ax) + + 2.0f * q0 * (2.0f * q0q1 + 2.0f * q2q3 - ay) + - 4.0f * q1 * (1.0f - 2.0f * q1q1 - 2.0f * q2q2 - az) + + twoBz * q3 * (twoBx * (0.5f - q2q2 - q3q3) + twoBz * (q1q3 - q0q2) - mx) + + (twoBx * q2 + twoBz * q0) * (twoBx * (q1q2 - q0q3) + twoBz * (q0q1 + q2q3) - my) + + (twoBx * q3 - fourBz * q1) * (twoBx * (q0q2 + q1q3) + twoBz * (0.5f - q1q1 - q2q2) - mz); + float s2 = -2.0f * q0 * (2.0f * q1q3 - 2.0f * q0q2 - ax) + + 2.0f * q3 * (2.0f * q0q1 + 2.0f * q2q3 - ay) + - 4.0f * q2 * (1.0f - 2.0f * q1q1 - 2.0f * q2q2 - az) + + (-fourBx * q2 - twoBz * q0) * (twoBx * (0.5f - q2q2 - q3q3) + twoBz * (q1q3 - q0q2) - mx) + + (twoBx * q1 + twoBz * q3) * (twoBx * (q1q2 - q0q3) + twoBz * (q0q1 + q2q3) - my) + + (twoBx * q0 - fourBz * q2) * (twoBx * (q0q2 + q1q3) + twoBz * (0.5f - q1q1 - q2q2) - mz); + float s3 = 2.0f * q1 * (2.0f * q1q3 - 2.0f * q0q2 - ax) + + 2.0f * q2 * (2.0f * q0q1 + 2.0f * q2q3 - ay) + + (-fourBx * q3 + twoBz * q1) * (twoBx * (0.5f - q2q2 - q3q3) + twoBz * (q1q3 - q0q2) - mx) + + (-twoBx * q0 + twoBz * q2) * (twoBx * (q1q2 - q0q3) + twoBz * (q0q1 + q2q3) - my) + + twoBx * q1 * (twoBx * (q0q2 + q1q3) + twoBz * (0.5f - q1q1 - q2q2) - mz); + + float stepNorm = invSqrt(s0 * s0 + s1 * s1 + s2 * s2 + s3 * s3); + if (!Float.isNaN(stepNorm) && !Float.isInfinite(stepNorm)) { + s0 *= stepNorm; + s1 *= stepNorm; + s2 *= stepNorm; + s3 *= stepNorm; + } else { + s0 = 0.0f; + s1 = 0.0f; + s2 = 0.0f; + s3 = 0.0f; + } + + float qDot0 = 0.5f * (-q1 * gx - q2 * gy - q3 * gz) - beta * s0; + float qDot1 = 0.5f * (q0 * gx + q2 * gz - q3 * gy) - beta * s1; + float qDot2 = 0.5f * (q0 * gy - q1 * gz + q3 * gx) - beta * s2; + float qDot3 = 0.5f * (q0 * gz + q1 * gy - q2 * gx) - beta * s3; + + q0 += qDot0 * deltaTimeSeconds; + q1 += qDot1 * deltaTimeSeconds; + q2 += qDot2 * deltaTimeSeconds; + q3 += qDot3 * deltaTimeSeconds; + + normalizeQuaternion(); + } + + public void updateIMU(float gx, float gy, float gz, + float ax, float ay, float az, + float deltaTimeSeconds) { + if (deltaTimeSeconds <= 0.0f) { + return; + } + + float accNorm = invSqrt(ax * ax + ay * ay + az * az); + if (Float.isNaN(accNorm) || Float.isInfinite(accNorm)) { + integrateGyroscopeOnly(gx, gy, gz, deltaTimeSeconds); + return; + } + + ax *= accNorm; + ay *= accNorm; + az *= accNorm; + integrateGyroAndAccelOnly(gx, gy, gz, ax, ay, az, deltaTimeSeconds); + } + + // Gyro + accelerometer integration path without magnetometer correction. + private void integrateGyroAndAccelOnly(float gx, float gy, float gz, + float ax, float ay, float az, + float deltaTimeSeconds) { + // Compute the expected vertical acceleration + float q0q0 = q0 * q0; + float q0q1 = q0 * q1; + float q0q2 = q0 * q2; + float q1q1 = q1 * q1; + float q1q2 = q1 * q2; + float q2q2 = q2 * q2; + float q3q2 = q3 * q2; + + // Error vector for gravity (accelerometer correction) + float ex = ay * (q0q0 - q1q1 - q2q2 + q3 * q3) - az * (2.0f * (q0q2 + q1 * q3)); + float ey = az * (2.0f * (q0q1 - q2 * q3)) - ax * (q0q0 - q1q1 - q2q2 + q3 * q3); + float ez = ax * (2.0f * (q1 * q3 - q0q2)) - ay * (2.0f * (q0q1 + q2q2)); + + // Reduce correction gain when heading cannot be constrained by magnetometer. + float conservativeBeta = this.beta * 0.8f; // Reduce correction strength + + // Integrate gyroscope with gravity correction (no magnetometer) + float qDot0 = 0.5f * (-q1 * gx - q2 * gy - q3 * gz) - conservativeBeta * ex; + float qDot1 = 0.5f * (q0 * gx + q2 * gz - q3 * gy) - conservativeBeta * ey; + float qDot2 = 0.5f * (q0 * gy - q1 * gz + q3 * gx) - conservativeBeta * ez; + float qDot3 = 0.5f * (q0 * gz + q1 * gy - q2 * gx); + + q0 += qDot0 * deltaTimeSeconds; + q1 += qDot1 * deltaTimeSeconds; + q2 += qDot2 * deltaTimeSeconds; + q3 += qDot3 * deltaTimeSeconds; + + normalizeQuaternion(); + } + + private void integrateGyroscopeOnly(float gx, float gy, float gz, float deltaTimeSeconds) { + float qDot0 = 0.5f * (-q1 * gx - q2 * gy - q3 * gz); + float qDot1 = 0.5f * (q0 * gx + q2 * gz - q3 * gy); + float qDot2 = 0.5f * (q0 * gy - q1 * gz + q3 * gx); + float qDot3 = 0.5f * (q0 * gz + q1 * gy - q2 * gx); + + q0 += qDot0 * deltaTimeSeconds; + q1 += qDot1 * deltaTimeSeconds; + q2 += qDot2 * deltaTimeSeconds; + q3 += qDot3 * deltaTimeSeconds; + normalizeQuaternion(); + } + + private void normalizeQuaternion() { + float quaternionNorm = invSqrt(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3); + if (Float.isNaN(quaternionNorm) || Float.isInfinite(quaternionNorm)) { + reset(); + return; + } + + q0 *= quaternionNorm; + q1 *= quaternionNorm; + q2 *= quaternionNorm; + q3 *= quaternionNorm; + } + + public float[] getEulerRadians() { + float yaw = (float) Math.atan2(2.0f * (q0 * q3 + q1 * q2), 1.0f - 2.0f * (q2 * q2 + q3 * q3)); + float sinPitch = 2.0f * (q0 * q2 - q3 * q1); + if (sinPitch > 1.0f) sinPitch = 1.0f; + if (sinPitch < -1.0f) sinPitch = -1.0f; + float pitch = (float) Math.asin(sinPitch); + float roll = (float) Math.atan2(2.0f * (q0 * q1 + q2 * q3), 1.0f - 2.0f * (q1 * q1 + q2 * q2)); + return new float[]{yaw, pitch, roll}; + } + + private float invSqrt(float value) { + if (value <= 0.0f) { + return Float.NaN; + } + return (float) (1.0 / Math.sqrt(value)); + } + + // Exposes anomaly count for runtime monitoring. + public int getMagneticAnomalyCount() { + return magneticAnomalyCount; + } +} + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/MagneticCompensation.java b/app/src/main/java/com/openpositioning/PositionMe/utils/MagneticCompensation.java new file mode 100644 index 00000000..d5e49a47 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/MagneticCompensation.java @@ -0,0 +1,89 @@ +package com.openpositioning.PositionMe.utils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +// Grid-based heading compensation for magnetic disturbances. +public class MagneticCompensation { + private final List gridCells; + private final double maxLookupDistanceMeters; + private final float maxCorrectionRad; + private volatile boolean enabled = true; + + public MagneticCompensation(List gridCells, + double maxLookupDistanceMeters, + float maxCorrectionRad) { + if (gridCells == null || gridCells.isEmpty()) { + this.gridCells = Collections.emptyList(); + } else { + this.gridCells = Collections.unmodifiableList(new ArrayList<>(gridCells)); + } + this.maxLookupDistanceMeters = Math.max(0.0, maxLookupDistanceMeters); + this.maxCorrectionRad = Math.max(0.0f, maxCorrectionRad); + } + + public static MagneticCompensation empty() { + return new MagneticCompensation(Collections.emptyList(), 10.0, (float) Math.toRadians(15.0)); + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + return enabled; + } + + public int getCellCount() { + return gridCells.size(); + } + + public float getCorrectionAngle(double currentX, double currentY) { + if (!enabled || gridCells.isEmpty()) { + return 0.0f; + } + + MagneticGridCell nearest = null; + double minDistance = Double.MAX_VALUE; + + for (MagneticGridCell cell : gridCells) { + double dx = cell.xMeters - currentX; + double dy = cell.yMeters - currentY; + double distance = Math.hypot(dx, dy); + if (distance < minDistance) { + minDistance = distance; + nearest = cell; + } + } + + if (nearest == null || minDistance > maxLookupDistanceMeters) { + return 0.0f; + } + + float confidence = clamp01(nearest.confidence); + float confidenceScaledCorrection = nearest.correctionRad * confidence; + return clamp(confidenceScaledCorrection, -maxCorrectionRad, maxCorrectionRad); + } + + private float clamp01(float value) { + if (value < 0.0f) { + return 0.0f; + } + if (value > 1.0f) { + return 1.0f; + } + return value; + } + + private float clamp(float value, float min, float max) { + if (value < min) { + return min; + } + if (value > max) { + return max; + } + return value; + } +} + diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/MagneticGridCell.java b/app/src/main/java/com/openpositioning/PositionMe/utils/MagneticGridCell.java new file mode 100644 index 00000000..95906f42 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/MagneticGridCell.java @@ -0,0 +1,26 @@ +package com.openpositioning.PositionMe.utils; + +// Immutable grid cell for magnetic heading compensation. +public class MagneticGridCell { + public final double xMeters; + public final double yMeters; + public final float correctionRad; + public final float confidence; + public final int sampleCount; + public final long updatedAtEpochMs; + + public MagneticGridCell(double xMeters, + double yMeters, + float correctionRad, + float confidence, + int sampleCount, + long updatedAtEpochMs) { + this.xMeters = xMeters; + this.yMeters = yMeters; + this.correctionRad = correctionRad; + this.confidence = confidence; + this.sampleCount = sampleCount; + this.updatedAtEpochMs = updatedAtEpochMs; + } +} + diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/MagneticGridLoader.java b/app/src/main/java/com/openpositioning/PositionMe/utils/MagneticGridLoader.java new file mode 100644 index 00000000..2b8bdc1b --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/MagneticGridLoader.java @@ -0,0 +1,91 @@ +package com.openpositioning.PositionMe.utils; + +import android.content.Context; +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +// Loads magnetic grid compensation data from assets JSON. +public final class MagneticGridLoader { + private static final String TAG = "MagneticGridLoader"; + + private MagneticGridLoader() { + } + + public static MagneticCompensation loadFromAssets(Context context, + String assetFileName, + double maxLookupDistanceMeters, + float maxCorrectionRad) { + if (context == null) { + return MagneticCompensation.empty(); + } + + try { + String json = readAssetText(context, assetFileName); + JSONArray cellsArray = parseGridArray(json); + List cells = new ArrayList<>(); + for (int i = 0; i < cellsArray.length(); i++) { + JSONObject item = cellsArray.optJSONObject(i); + if (item == null) { + continue; + } + + double x = item.optDouble("x", Double.NaN); + double y = item.optDouble("y", Double.NaN); + float correctionRad = (float) item.optDouble("correctionRad", 0.0); + float confidence = (float) item.optDouble("confidence", 1.0); + int sampleCount = item.optInt("sampleCount", 0); + long updatedAt = item.optLong("updatedAt", 0L); + + if (Double.isNaN(x) || Double.isNaN(y) || Float.isNaN(correctionRad) || Float.isInfinite(correctionRad)) { + continue; + } + + cells.add(new MagneticGridCell(x, y, correctionRad, confidence, sampleCount, updatedAt)); + } + + Log.i(TAG, "Loaded magnetic grid cells: " + cells.size()); + return new MagneticCompensation(cells, maxLookupDistanceMeters, maxCorrectionRad); + } catch (Exception e) { + Log.w(TAG, "Failed to load magnetic grid from assets, compensation disabled", e); + return MagneticCompensation.empty(); + } + } + + private static String readAssetText(Context context, String assetFileName) throws IOException { + try (InputStream inputStream = context.getAssets().open(assetFileName); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + byte[] buffer = new byte[4096]; + int read; + while ((read = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, read); + } + return outputStream.toString(StandardCharsets.UTF_8.name()); + } + } + + private static JSONArray parseGridArray(String json) throws JSONException { + String trimmed = json == null ? "" : json.trim(); + if (trimmed.startsWith("[")) { + return new JSONArray(trimmed); + } + + JSONObject root = new JSONObject(trimmed); + JSONArray grid = root.optJSONArray("grid"); + if (grid != null) { + return grid; + } + + return new JSONArray(); + } +} + diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/NucleusBuildingManager.java b/app/src/main/java/com/openpositioning/PositionMe/utils/NucleusBuildingManager.java index 3570e8ad..341cef49 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/utils/NucleusBuildingManager.java +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/NucleusBuildingManager.java @@ -43,12 +43,9 @@ public IndoorMapFragment getIndoorMapManager() { return indoorMapFragment; } - /** - * Determines if a given point is inside the building polygon. - * - * @param point the point to check - * @return true if the point is inside the polygon, false otherwise - */ + // Determines if a given point is inside the building polygon. +// Parameter point: the point to check +// Returns: true if the point is inside the polygon, false otherwise public boolean isPointInBuilding(LatLng point) { int intersectCount = 0; // Loop through each edge of the polygon @@ -64,14 +61,11 @@ public boolean isPointInBuilding(LatLng point) { return ((intersectCount % 2) == 1); // odd = inside, even = outside; } - /** - * Determines if a ray from a point intersects with a given edge of the polygon. - * - * @param point the point from which the ray is cast - * @param vertA the first vertex of the edge - * @param vertB the second vertex of the edge - * @return true if the ray intersects with the edge, false otherwise - */ + // Determines if a ray from a point intersects with a given edge of the polygon. +// Parameter point: the point from which the ray is cast +// Parameter vertA: the first vertex of the edge +// Parameter vertB: the second vertex of the edge +// Returns: true if the ray intersects with the edge, false otherwise private boolean rayCastIntersect(LatLng point, LatLng vertA, LatLng vertB) { double aY = vertA.latitude; double bY = vertB.latitude; @@ -96,3 +90,5 @@ private boolean rayCastIntersect(LatLng point, LatLng vertA, LatLng vertB) { return x > pX; } } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/PathView.java b/app/src/main/java/com/openpositioning/PositionMe/utils/PathView.java index 5a5efa8d..c18a3711 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/utils/PathView.java +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/PathView.java @@ -8,23 +8,18 @@ import android.util.AttributeSet; import android.view.View; -import com.openpositioning.PositionMe.presentation.fragment.CorrectionFragment; import com.openpositioning.PositionMe.sensors.SensorFusion; import java.util.ArrayList; import java.util.Collections; -/** - * This View class displays the path taken in the UI. - * A path of straight lines is drawn based on PDR coordinates. The coordinates are passed to - * PathView by calling method {@link PathView#drawTrajectory(float[])} in {@link SensorFusion}. - * The coordinates are scaled and centered in {@link PathView#scaleTrajectory()} to fill the - * device's screen. The scaling ratio is passed to the {@link CorrectionFragment} for calculating - * the Google Maps zoom ratio. - * - * @author Michal Dvorak - * @author Virginia Cangelosi - */ +// This View class displays the path taken in the UI. +// A path of straight lines is drawn based on PDR coordinates. The coordinates are passed to +// PathView by calling method {@link PathView#drawTrajectory(float[])} in {@link SensorFusion}. +// The coordinates are scaled and centered in {@link PathView#scaleTrajectory()} to fill the +// device's screen. +// @author Michal Dvorak +// @author Virginia Cangelosi public class PathView extends View { // Set up drawing colour private final int paintColor = Color.BLUE; @@ -37,23 +32,19 @@ public class PathView extends View { private static ArrayList yCoords = new ArrayList(); // Scaling ratio for multiplying PDR coordinates to fill the screen size private static float scalingRatio; - // Instantiate correction fragment for passing it the scaling ratio - private CorrectionFragment correctionFragment = new CorrectionFragment(); + // Boolean flag to avoid rescaling trajectory when view is redrawn private static boolean firstTimeOnDraw = true; - //Variable to only draw when the variable is true + // Variable to only draw when the variable is true private static boolean draw = true; - //Variable to only draw when the variable is true + // Variable to only draw when the variable is true private static boolean reDraw = false; - /** - * Public default constructor for PathView. The constructor initialises the view with a context - * and attribute set, sets the view as focusable and focusable in touch mode and calls - * {@link PathView#setupPaint()} to initialise the paint object with colour and style. - * - * @param context Application Context to be used for permissions and device accesses. - * @param attrs The attribute set of the view. - */ + // Public default constructor for PathView. The constructor initialises the view with a context + // and attribute set, sets the view as focusable and focusable in touch mode and calls + // {@link PathView#setupPaint()} to initialise the paint object with colour and style. +// Parameter context: Application Context to be used for permissions and device accesses. +// Parameter attrs: The attribute set of the view. public PathView(Context context, AttributeSet attrs) { super(context, attrs); setFocusable(true); @@ -61,9 +52,7 @@ public PathView(Context context, AttributeSet attrs) { setupPaint(); } - /** - * Method used for setting up paint object for drawing the path with colour and stroke styles. - */ + // Method used for setting up paint object for drawing the path with colour and stroke styles. private void setupPaint() { drawPaint = new Paint(); // Set the color of the paint object to paintColor @@ -80,23 +69,19 @@ private void setupPaint() { drawPaint.setStrokeCap(Paint.Cap.ROUND); } - /** - * {@inheritDoc} - * - * Method drawing the created path with our paint. - * - * @param canvas The canvas on which the path will be drawn - */ + // {@inheritDoc} + // Method drawing the created path with our paint. +// Parameter canvas: The canvas on which the path will be drawn @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); - //If drawing for first time scale trajectory to fit screen + // If drawing for first time scale trajectory to fit screen if(this.draw){ // If there are no coordinates, don't draw anything if (xCoords.size() == 0) return; - //Scale trajectory to fit screen + // Scale trajectory to fit screen scaleTrajectory(); // Start a new path at the center of the view @@ -107,20 +92,20 @@ protected void onDraw(Canvas canvas) { path.lineTo(xCoords.get(i), yCoords.get(i)); } - //Draw path + // Draw path canvas.drawPath(path, drawPaint); - //Ensure path not redrawn + // Ensure path not redrawn draw = false; } - //If redrawing due to scaling of the average step length + // If redrawing due to scaling of the average step length else if(reDraw){ // If there are no coordinates, don't draw anything if (xCoords.size() == 0) return; - //Clear old path + // Clear old path path.reset(); // Iterate over all coordinates, shifting to the center and scaling then returning to original location @@ -141,7 +126,7 @@ else if(reDraw){ canvas.drawPath(path, drawPaint); - //Ensure path not redrawn when screen is resized + // Ensure path not redrawn when screen is resized reDraw = false; } else{ @@ -162,12 +147,9 @@ else if(reDraw){ } } - /** - * Method called from {@link SensorFusion} used for adding PDR coordinates to the path to be - * drawn. - * - * @param newCords An array containing the newly calculated coordinates to be added. - */ + // Method called from {@link SensorFusion} used for adding PDR coordinates to the path to be + // drawn. +// Parameter newCords: An array containing the newly calculated coordinates to be added. public void drawTrajectory(float[] newCords) { // Add x coordinates xCoords.add(newCords[0]); @@ -176,11 +158,9 @@ public void drawTrajectory(float[] newCords) { yCoords.add(-newCords[1]); } - /** - * Method used for scaling PDR coordinates to fill the screen. - * Center of the view is used as the origin, scaling ratio is calculated for the path to fit - * the screen with margins included. - */ + // Method used for scaling PDR coordinates to fill the screen. + // Center of the view is used as the origin, scaling ratio is calculated for the path to fit + // the screen with margins included. private void scaleTrajectory() { // Get the center coordinates of the view int centerX = getWidth() / 2; @@ -204,9 +184,6 @@ private void scaleTrajectory() { } System.out.println("Adjusted scaling ratio: " + scalingRatio); - // Set the scaling ratio for the correction fragment for setting Google Maps zoom - correctionFragment.setScalingRatio(scalingRatio); - // Iterate over all coordinates, shifting to the center and scaling for (int i = 0; i < xCoords.size(); i++) { float newXCoord = xCoords.get(i) * scalingRatio + centerX; @@ -216,32 +193,27 @@ private void scaleTrajectory() { } } - /** - * Method called when PathView is detached from its window. {@link PathView#xCoords} and - * {@link PathView#yCoords} are cleared so that path can start from 0 for next recording. - */ + // Method called when PathView is detached from its window. {@link PathView#xCoords} and + // {@link PathView#yCoords} are cleared so that path can start from 0 for next recording. @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); // Reset trajectory xCoords.clear(); yCoords.clear(); - //New recording so must scale trajectory + // New recording so must scale trajectory draw = true; } - /** - * Redraw trajectory to rescale the path. - * Called by {@link CorrectionFragment} through {@link SensorFusion} to reset the scaling ratio - * which will resize the path. It enables the redraw flag so new path is drawn. - * - * @param newScale - */ + // Redraw trajectory to rescale the path. + // Called by {@link CorrectionFragment} through {@link SensorFusion} to reset the scaling ratio + // which will resize the path. It enables the redraw flag so new path is drawn. + // Parameter newScale: scaling factor applied to stored path coordinates. public void redraw(float newScale){ - //Set scaling ratio based on user input + // Set scaling ratio based on user input scalingRatio = newScale; - //Enable redrawing of path + // Enable redrawing of path reDraw = true; } - } + diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/PdrParameterOptimization.java b/app/src/main/java/com/openpositioning/PositionMe/utils/PdrParameterOptimization.java new file mode 100644 index 00000000..e033e030 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/PdrParameterOptimization.java @@ -0,0 +1,231 @@ +package com.openpositioning.PositionMe.utils; + + + + +public class PdrParameterOptimization { + + + public static class CurrentParameters { + + public static final float K = 0.364f; + + + public static final double Q_XX = 0.5; + public static final double Q_YY = 0.5; + public static final double R_GNSS_XX = 25.0; + public static final double R_GNSS_YY = 25.0; + public static final double R_WIFI_XX = 9.0; + public static final double R_WIFI_YY = 9.0; + public static final double P_INIT_XX = 10.0; + public static final double P_INIT_YY = 10.0; + + // Madgwick AHRS + public static final float MADGWICK_BETA = 0.12f; + } + + + public static class ImprovedParameters_Phase1 { + + public static final float K_BASE = 0.364f; + + + public static class StepFrequencyClass { + public static final float SLOW_WALK_THRESHOLD = 1.2f; // Hz + public static final float SLOW_WALK_FACTOR = 0.85f; + + public static final float NORMAL_WALK_THRESHOLD = 1.8f; // Hz + public static final float NORMAL_WALK_FACTOR = 1.0f; + + public static final float FAST_WALK_THRESHOLD = 2.2f; // Hz + public static final float FAST_WALK_FACTOR = 1.1f; + + public static final float VERY_FAST_FACTOR = 1.2f; + } + + + public static final double Q_XX = 0.01; + public static final double Q_YY = 0.01; + public static final double R_GNSS_XX = 4.0; + public static final double R_GNSS_YY = 4.0; + public static final double R_WIFI_XX = 36.0; + public static final double R_WIFI_YY = 36.0; + public static final double P_INIT_XX = 1.0; + public static final double P_INIT_YY = 1.0; + + + public static final float MADGWICK_BETA_DEFAULT = 0.15f; + public static final float MADGWICK_BETA_HIGH_MOTION = 0.30f; + public static final float MADGWICK_BETA_LOW_MOTION = 0.10f; + + + public static final float MAGNETOMETER_MIN_UT = 20.0f; + public static final float MAGNETOMETER_MAX_UT = 60.0f; + } + + + public static class ImprovedParameters_Phase2 { + + public static class UserAdaptation { + + + public static final float[] HEIGHT_CM = {150, 160, 170, 180, 190}; + public static final float[] STRIDE_FACTOR = {0.80f, 0.90f, 1.0f, 1.10f, 1.25f}; + + + public static final float[] WEIGHT_KG = {50, 65, 80, 95}; + public static final float[] ACCEL_SENSITIVITY = {1.15f, 1.0f, 0.90f, 0.80f}; + } + + + public static class NonlinearStrideModel { + + // stride_length = a0 + a1frequency + a2frequency^2 + a3sqrt(bounce) + public static final float A0 = 0.1f; + public static final float A1 = 0.3f; + public static final float A2 = -0.05f; + public static final float A3 = 0.5f; + } + + + public static class CorridorAlignment { + public static final float CORRIDOR_SNAP_THRESHOLD = 1.5f; + public static final float CORRIDOR_DIRECTION_TOLERANCE = 15.0f; + } + + + public static class MultiHypothesisTracking { + public static final int NUM_HYPOTHESES = 3; + public static final float HYPOTHESIS_PRUNING_RATIO = 0.1f; + } + } + + + public static void printParameterComparison() { + System.out.println("=== PDR Parameter Optimization Comparison ===\n"); + + System.out.println("[Weiberg Algorithm]"); + System.out.println("Current: K = " + CurrentParameters.K + " (fixed value)"); + System.out.println("Improved: K = " + ImprovedParameters_Phase1.K_BASE + + " x step-frequency classifier (0.85~1.2)"); + System.out.println("Expected gain: +3% to +5% stride estimation accuracy\n"); + + System.out.println("[EKF Process Noise (Q)]"); + System.out.println("Current: Q_xx = " + CurrentParameters.Q_XX + + " (over-trusts GNSS/WiFi)"); + System.out.println("Improved: Q_xx = " + ImprovedParameters_Phase1.Q_XX + + " (more trust in PDR internal dynamics)"); + System.out.println("Expected gain: 50% to 70% fewer fusion jumps\n"); + + System.out.println("[EKF Measurement Noise (R_WIFI)]"); + System.out.println("Current: R_WIFI = " + CurrentParameters.R_WIFI_XX + + " m^2 (sigma=3m) - too optimistic"); + System.out.println("Improved: R_WIFI = " + ImprovedParameters_Phase1.R_WIFI_XX + + " m^2 (sigma=6m) - more realistic"); + System.out.println("Expected gain: 50% lower impact from WiFi outliers\n"); + + System.out.println("[Madgwick Beta]"); + System.out.println("Current: beta = " + CurrentParameters.MADGWICK_BETA + + " (fixed value, may be too low)"); + System.out.println("Improved: beta = " + ImprovedParameters_Phase1.MADGWICK_BETA_DEFAULT + + " (adaptive range: 0.10~0.30)"); + System.out.println("Expected gain: 30% to 40% less gyro drift\n"); + + System.out.println("[Initial Covariance (P)]"); + System.out.println("Current: P_init = " + CurrentParameters.P_INIT_XX); + System.out.println("Improved: P_init = " + ImprovedParameters_Phase1.P_INIT_XX); + System.out.println("Expected gain: around 3x faster EKF convergence\n"); + } + + + public static float classifyStepFrequency(float stepFrequencyHz) { + if (stepFrequencyHz < ImprovedParameters_Phase1.StepFrequencyClass.SLOW_WALK_THRESHOLD) { + return ImprovedParameters_Phase1.StepFrequencyClass.SLOW_WALK_FACTOR; + } else if (stepFrequencyHz < ImprovedParameters_Phase1.StepFrequencyClass.NORMAL_WALK_THRESHOLD) { + return ImprovedParameters_Phase1.StepFrequencyClass.NORMAL_WALK_FACTOR; + } else if (stepFrequencyHz < ImprovedParameters_Phase1.StepFrequencyClass.FAST_WALK_THRESHOLD) { + return ImprovedParameters_Phase1.StepFrequencyClass.FAST_WALK_FACTOR; + } else { + return ImprovedParameters_Phase1.StepFrequencyClass.VERY_FAST_FACTOR; + } + } + + + public static float getUserHeightAdaptation(int userHeightCm) { + + float[] heights = ImprovedParameters_Phase2.UserAdaptation.HEIGHT_CM; + float[] factors = ImprovedParameters_Phase2.UserAdaptation.STRIDE_FACTOR; + + if (userHeightCm <= heights[0]) { + return factors[0]; + } + if (userHeightCm >= heights[heights.length - 1]) { + return factors[factors.length - 1]; + } + + for (int i = 0; i < heights.length - 1; i++) { + if (userHeightCm >= heights[i] && userHeightCm < heights[i + 1]) { + float ratio = (userHeightCm - heights[i]) / (heights[i + 1] - heights[i]); + return factors[i] * (1 - ratio) + factors[i + 1] * ratio; + } + } + + return 1.0f; + } + + + public static float calculateImprovedStride( + double maxAccel, double minAccel, + long stepDurationMs, int userHeightCm) { + + + float bounce = (float) Math.pow((maxAccel - minAccel), 0.25); + float baseStride = bounce * ImprovedParameters_Phase1.K_BASE * 2; + + + float stepFreq = stepDurationMs > 0 ? 1000f / stepDurationMs : 1.8f; + float frequencyFactor = classifyStepFrequency(stepFreq); + + + float heightFactor = getUserHeightAdaptation(userHeightCm); + + + float finalStride = baseStride * frequencyFactor * heightFactor; + + + if (finalStride < 0.3f || finalStride > 1.2f) { + return 0.75f; + } + + return finalStride; + } + + + public static void printOptimizationSummary() { + System.out.println("\n============================================"); + System.out.println(" PDR Drift Reduction - Action Plan"); + System.out.println("============================================"); + System.out.println("[P1 - 5 minutes] Tune EKF parameters"); + System.out.println(" - Q: 0.5 -> 0.01"); + System.out.println(" - R_WIFI: 9 -> 36"); + System.out.println(" - P_init: 10 -> 1"); + System.out.println(); + System.out.println("[P2 - 10 minutes] Detect magnetic anomalies"); + System.out.println(" - Check magnetic field range: 20~60 uT"); + System.out.println(" - If abnormal, reduce magnetometer influence"); + System.out.println(); + System.out.println("[P3 - 15 minutes] Enable step-frequency classifier"); + System.out.println(" - 4 walking-speed classes"); + System.out.println(" - Correction factors: 0.85~1.2"); + System.out.println(); + System.out.println("Expected total improvement: 30% to 50% less drift"); + System.out.println("============================================"); + } + + public static void main(String[] args) { + printParameterComparison(); + printOptimizationSummary(); + } +} + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/PdrProcessing.java b/app/src/main/java/com/openpositioning/PositionMe/utils/PdrProcessing.java index 9765b044..ba3de871 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/utils/PdrProcessing.java +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/PdrProcessing.java @@ -3,11 +3,16 @@ import android.content.Context; import android.content.SharedPreferences; import android.hardware.SensorManager; +import android.util.Log; import androidx.preference.PreferenceManager; +import com.google.android.gms.maps.model.LatLng; import com.openpositioning.PositionMe.sensors.SensorFusion; +import com.openpositioning.PositionMe.utils.BuildingPolygon; +import com.openpositioning.PositionMe.utils.GeometryUtils; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -15,18 +20,15 @@ import java.util.Objects; import java.util.stream.Collectors; -/** - * Processes data recorded in the {@link SensorFusion} class and calculates live PDR estimates. - * It calculates the position from the steps and directions detected, using either estimated values - * (eg. stride length from the Weiberg algorithm) or provided constants, calculates the elevation - * and attempts to estimate the current floor as well as elevators. - * - * @author Mate Stodulka - * @author Michal Dvorak - */ +// Processes data recorded in the {@link SensorFusion} class and calculates live PDR estimates. +// It calculates the position from the steps and directions detected, using either estimated values +// (eg. stride length from the Weiberg algorithm) or provided constants, calculates the elevation +// and attempts to estimate the current floor as well as elevators. +// @author Mate Stodulka +// @author Michal Dvorak public class PdrProcessing { - //region Static variables + // region Static variables // Weiberg algorithm coefficient for stride calculations private static final float K = 0.364f; // Number of samples (seconds) to keep as memory for elevation calculation @@ -38,9 +40,18 @@ public class PdrProcessing { // Threshold under which movement is considered non-existent private static final float epsilon = 0.18f; private static final int MIN_REQUIRED_SAMPLES = 2; - //endregion + private static final float FLOOR_SWITCH_ENTER_RATIO = 0.65f; + private static final float FLOOR_SWITCH_STRONG_PROGRESS_RATIO = 0.9f; + private static final float FLOOR_BASELINE_RECENTER_RATIO = 0.28f; + private static final float ANCHORED_FLOOR_SWITCH_ENTER_RATIO = 0.35f; + private static final float ANCHORED_FLOOR_SWITCH_STRONG_PROGRESS_RATIO = 0.45f; + private static final int FLOOR_SWITCH_CONFIRMATIONS = 2; + private static final int MAX_SINGLE_FLOOR_JUMP = 1; + private static final double DEFAULT_MAG_LOOKUP_DISTANCE_METERS = 10.0; + private static final float DEFAULT_MAG_MAX_CORRECTION_RAD = (float) Math.toRadians(15.0); + // endregion - //region Instance variables + // region Instance variables // Settings for accessing shared variables private SharedPreferences settings; @@ -49,16 +60,69 @@ public class PdrProcessing { // Using manually input constants instead of estimated values private boolean useManualStep; - // Current 2D position coordinates + // Current 2D position coordinates (raw PDR) private float positionX; private float positionY; + // Fused 2D position coordinates in East/North meters (sensor fusion result) + private double fusedX; + private double fusedY; + + private long previousStepEnd = -1; + + // Motion analysis for stairs/elevator detection + private float lastHorizontalMovement = 0f; + private boolean isElevatorMotion = false; + private boolean isStairsMotion = false; + private float lastSmoothedRelativeElevation; + private List> indoorStairsZones = new java.util.ArrayList<>(); + private List> indoorLiftZones = new java.util.ArrayList<>(); + private List> indoorWalls = new java.util.ArrayList<>(); + private float[] floorAltitudeAnchorsMeters = null; + private int pendingFloorCandidate = Integer.MIN_VALUE; + private int pendingFloorCandidateCount = 0; + private boolean initialAnchorFloorResolved = false; + + // Predefined map elements for stairs/elevator zones (approximate coordinates) + private static final float MAP_ZONE_RADIUS_METERS = 8.0f; + private static final LatLng[] STAIRS_ZONES = new LatLng[] { + new LatLng(55.92305, -3.17420), // nucleus stair + new LatLng(55.92310, -3.17400) // library stair + }; + private static final LatLng[] LIFT_ZONES = new LatLng[] { + new LatLng(55.92310, -3.17395), // nucleus lift + new LatLng(55.92302, -3.17410) // library lift + }; + + // Kalman filter covariances for fusion + private double[][] P; + // Kalman filter noise settings tuned for PDR-dominant indoor fusion. + private final double[][] Q = {{0.01, 0}, {0, 0.01}}; // process noise (decreased from 0.5) + private final double[][] R_GNSS = {{4, 0}, {0, 4}}; + private final double[][] R_WIFI = {{36, 0}, {0, 36}}; + + // GNSS/WiFi correction limits to keep heading and path direction PDR-dominant. + private static final float GNSS_ERROR_THRESHOLD_METERS = 5.0f; + private static final float WIFI_ERROR_THRESHOLD_METERS = 3.0f; + private static final float GNSS_MIN_ACCURACY_METERS = 6.0f; + private static final float GNSS_STABLE_RADIUS_METERS = 1.2f; + private static final int GNSS_STABLE_CONFIRMATIONS_REQUIRED = 2; + private static final double GNSS_POSITION_NUDGE_FACTOR = 0.30; // 2% (previously 1%) + private static final double WIFI_POSITION_NUDGE_FACTOR = 0.18; // 1% ultra-light WiFi pull + private static final float MIN_EFFECTIVE_STEP_DISPLACEMENT_METERS = 0.28f; + private static final double GNSS_MAX_CORRECTION_PER_UPDATE_METERS = 0.60; + private static final double WIFI_MAX_CORRECTION_PER_UPDATE_METERS = 0.50; + + private boolean fusionInitialized = false; + private double originLat; + private double originLon; + // Vertical movement calculation private Float[] startElevationBuffer; private float startElevation; private int setupIndex = 0; private float elevation; - private int floorHeight; + private float floorHeight; private int currentFloor; // Buffer of most recent elevations calculated @@ -71,14 +135,24 @@ public class PdrProcessing { // Step sum and length aggregation variables private float sumStepLength = 0; private int stepCount = 0; - //endregion - - /** - * Public constructor for the PDR class. - * Takes context for variable access. Sets initial values based on settings. - * - * @param context Application context for variable access. - */ + + // Keep track of the most recent coordinates for each source. + private static final int MAX_COORDINATE_HISTORY = 7; + // History buffers for GNSS, WiFi, and PDR coordinates (stores LatLng) + private java.util.Queue gnssHistoryBuffer = new java.util.LinkedList<>(); + private java.util.Queue wifiHistoryBuffer = new java.util.LinkedList<>(); + private java.util.Queue pdrHistoryBuffer = new java.util.LinkedList<>(); + private LatLng lastStableGnssCandidate = null; + private int stableGnssCandidateCount = 0; + + // Magnetic heading compensation (grid lookup) + private MagneticCompensation magneticCompensation = MagneticCompensation.empty(); + private boolean enableMagneticCompensation = true; + // endregion + + // Public constructor for the PDR class. + // Takes context for variable access. Sets initial values based on settings. +// Parameter context: Application context for variable access. public PdrProcessing(Context context) { // Initialise settings this.settings = PreferenceManager.getDefaultSharedPreferences(context); @@ -86,7 +160,7 @@ public PdrProcessing(Context context) { this.useManualStep = this.settings.getBoolean("manual_step_values", false); if(useManualStep) { try { - // Retrieve manual step length + // Retrieve manual step length this.stepLength = this.settings.getInt("user_step_length", 75) / 100f; } catch (Exception e) { // Invalid values - reset to defaults @@ -128,26 +202,87 @@ public PdrProcessing(Context context) { this.startElevationBuffer = new Float[3]; // Start floor - assumed to be zero this.currentFloor = 0; + + initializeMagneticCompensation(context); + } + + // Re-read settings from SharedPreferences (e.g. step length). + // Call this when starting recording to ensure latest settings are used. + public void refreshSettings() { + this.useManualStep = this.settings.getBoolean("manual_step_values", false); + if(useManualStep) { + try { + this.stepLength = this.settings.getInt("user_step_length", 75) / 100f; + } catch (Exception e) { + this.stepLength = 0.75f; + } + } else { + this.stepLength = 0; + } + + this.enableMagneticCompensation = this.settings.getBoolean("enable_magnetic_compensation", true); + if (this.magneticCompensation != null) { + this.magneticCompensation.setEnabled(this.enableMagneticCompensation); + } + } + + private void initializeMagneticCompensation(Context context) { + this.enableMagneticCompensation = this.settings.getBoolean("enable_magnetic_compensation", true); + this.magneticCompensation = MagneticGridLoader.loadFromAssets( + context, + "magnetic_grid.json", + DEFAULT_MAG_LOOKUP_DISTANCE_METERS, + DEFAULT_MAG_MAX_CORRECTION_RAD + ); + this.magneticCompensation.setEnabled(this.enableMagneticCompensation); + Log.d("PdrProcessing", "Magnetic compensation loaded: cells=" + this.magneticCompensation.getCellCount() + + ", enabled=" + this.enableMagneticCompensation); + } + + private float applyHeadingCompensation(float headingRad) { + if (!enableMagneticCompensation || magneticCompensation == null) { + return headingRad; + } + + double currentX = this.fusionInitialized ? this.fusedX : this.positionX; + double currentY = this.fusionInitialized ? this.fusedY : this.positionY; + float correctionRad = magneticCompensation.getCorrectionAngle(currentX, currentY); + if (Float.isNaN(correctionRad) || Float.isInfinite(correctionRad)) { + correctionRad = 0.0f; + } + + float corrected = normalizeRadians(headingRad + correctionRad); + if (Math.abs(correctionRad) > 0.01f) { + Log.d("PdrProcessing", String.format( + "Heading compensation applied: raw=%.3f rad, correction=%.3f rad, corrected=%.3f rad", + headingRad, correctionRad, corrected)); + } + return corrected; + } + + private float normalizeRadians(float angleRad) { + float twoPi = (float) (Math.PI * 2.0); + float normalized = angleRad % twoPi; + if (normalized > Math.PI) { + normalized -= twoPi; + } else if (normalized < -Math.PI) { + normalized += twoPi; + } + return normalized; } - /** - * Function to calculate PDR coordinates from sensor values. - * Should be called from the step detector sensor's event with the sensor values since the last - * step. - * - * @param currentStepEnd relative time in milliseconds since the start of the recording. - * @param accelMagnitudeOvertime recorded acceleration magnitudes since the last step. - * @param headingRad heading relative to magnetic north in radians. - */ + // Function to calculate PDR coordinates from sensor values. + // Should be called from the step detector sensor's event with the sensor values since the last + // step. +// Parameter currentStepEnd: relative time in milliseconds since the start of the recording. +// Parameter accelMagnitudeOvertime: recorded acceleration magnitudes since the last step. +// Parameter headingRad: heading relative to magnetic north in radians. public float[] updatePdr(long currentStepEnd, List accelMagnitudeOvertime, float headingRad) { if (accelMagnitudeOvertime == null || accelMagnitudeOvertime.size() < MIN_REQUIRED_SAMPLES) { - return new float[]{this.positionX, this.positionY}; // Return current position without update - // - TODO - temporary solution of the empty list issue + // Preserve the previous position when there are not enough samples to estimate a step. + return new float[]{this.positionX, this.positionY}; } - // Change angle so zero rad is east - float adaptedHeading = (float) (Math.PI/2 - headingRad); - // check if accelMagnitudeOvertime is empty if (accelMagnitudeOvertime == null || accelMagnitudeOvertime.isEmpty()) { // return current position, do not update @@ -156,9 +291,15 @@ public float[] updatePdr(long currentStepEnd, List accelMagnitudeOvertim // Calculate step length if(!useManualStep) { - //ArrayList accelMagnitudeFiltered = filter(accelMagnitudeOvertime); + long stepDurationMs = -1; + if (this.previousStepEnd != -1 && currentStepEnd > this.previousStepEnd) { + stepDurationMs = currentStepEnd - this.previousStepEnd; + } + this.previousStepEnd = currentStepEnd; + + // ArrayList accelMagnitudeFiltered = filter(accelMagnitudeOvertime); // Estimate stride - this.stepLength = weibergMinMax(accelMagnitudeOvertime); + this.stepLength = weibergMinMax(accelMagnitudeOvertime, stepDurationMs); // System.err.println("Step Length" + stepLength); } @@ -166,27 +307,101 @@ public float[] updatePdr(long currentStepEnd, List accelMagnitudeOvertim sumStepLength += stepLength; stepCount++; + // Apply position-aware heading correction from magnetic grid. + float correctedHeadingRad = applyHeadingCompensation(headingRad); + + // Change angle so zero rad is east + float adaptedHeading = (float) (Math.PI/2 - correctedHeadingRad); + // Translate to cartesian coordinate system float x = (float) (stepLength * Math.cos(adaptedHeading)); float y = (float) (stepLength * Math.sin(adaptedHeading)); + float stepDisplacement = (float) Math.hypot(x, y); + if (stepDisplacement < MIN_EFFECTIVE_STEP_DISPLACEMENT_METERS) { + Log.d("PdrProcessing", String.format( + "Micro-step suppressed: %.3fm < %.3fm", + stepDisplacement, MIN_EFFECTIVE_STEP_DISPLACEMENT_METERS)); + return new float[]{this.positionX, this.positionY}; + } + + // Update position values + this.positionX += x; + this.positionY += y; + + // Motion classification + this.lastHorizontalMovement = (float)Math.hypot(x, y); + this.isStairsMotion = this.lastHorizontalMovement > 0.5f; + // Elevator motion monitored in estimateElevator() from barometer/accelerometer pattern + + // EKF fusion prediction step (PDR motion model in East/North meters) + if (this.fusionInitialized) { + predictFusion(x, y); + } + + // Save current PDR position in history buffer. + LatLng pdrPoint = localToLatLon(this.positionX, this.positionY); + pdrHistoryBuffer.offer(pdrPoint); + if (pdrHistoryBuffer.size() > MAX_COORDINATE_HISTORY) { + pdrHistoryBuffer.poll(); + } + + // return raw PDR position + return new float[]{this.positionX, this.positionY}; + } + + // Update PDR with a specific step length (e.g. from Weinberg algorithm). +// Parameter stepLengthMeters: The calculated stride length in meters. +// Parameter headingRad: Current heading in radians. +// Returns: New [x, y] coordinates. + public float[] updatePdrWithStride(float stepLengthMeters, float headingRad) { + // Apply position-aware heading correction from magnetic grid. + float correctedHeadingRad = applyHeadingCompensation(headingRad); + + // Change angle so zero rad is east + float adaptedHeading = (float) (Math.PI/2 - correctedHeadingRad); + + // Increment aggregate variables + this.sumStepLength += stepLengthMeters; + this.stepCount++; + + // Translate to cartesian coordinate system + float x = (float) (stepLengthMeters * Math.cos(adaptedHeading)); + float y = (float) (stepLengthMeters * Math.sin(adaptedHeading)); + + float stepDisplacement = (float) Math.hypot(x, y); + if (stepDisplacement < MIN_EFFECTIVE_STEP_DISPLACEMENT_METERS) { + Log.d("PdrProcessing", String.format( + "Micro-step suppressed (manual stride): %.3fm < %.3fm", + stepDisplacement, MIN_EFFECTIVE_STEP_DISPLACEMENT_METERS)); + return new float[]{this.positionX, this.positionY}; + } + // Update position values this.positionX += x; this.positionY += y; - // return current position + // Fusion prediction step + if (this.fusionInitialized) { + predictFusion(x, y); + } + + // Save current PDR position in history buffer. + LatLng pdrPoint = localToLatLon(this.positionX, this.positionY); + pdrHistoryBuffer.offer(pdrPoint); + if (pdrHistoryBuffer.size() > MAX_COORDINATE_HISTORY) { + pdrHistoryBuffer.poll(); + } + return new float[]{this.positionX, this.positionY}; } - /** - * Calculates the relative elevation compared to the start position. - * The start elevation is the median of the first three seconds of data to give the sensor time - * to settle. The sea level is irrelevant as only values relative to the initial position are - * reported. - * - * @param absoluteElevation absolute elevation in meters compared to sea level. - * @return current elevation in meters relative to the start position. - */ + // Calculates the relative elevation compared to the start position. + // The start elevation is the median of the first three seconds of data to give the sensor time + // to settle. The sea level is irrelevant as only values relative to the initial position are + // reported. +// Parameter absoluteElevation: absolute elevation in meters compared to sea level. +// Returns: current elevation in meters relative to the start position. public float updateElevation(float absoluteElevation) { // Set start to median of first three values if(setupIndex < 3) { @@ -200,39 +415,84 @@ public float updateElevation(float absoluteElevation) { this.setupIndex++; } else { - // Get relative elevation in meters - this.elevation = absoluteElevation - startElevation; - // Add to buffer + // Add raw absolute elevation to buffer for smoothing this.elevationList.putNewest(absoluteElevation); - // Check if there was floor movement - // Check if there is enough data to evaluate + // Update floor estimation from barometer-based height if(this.elevationList.isFull()) { - // Check average of elevation array + // Use stabilized average over the recent window to avoid noise. List elevationMemory = this.elevationList.getListCopy(); OptionalDouble currentAvg = elevationMemory.stream().mapToDouble(f -> f).average(); - float finishAvg = currentAvg.isPresent() ? (float) currentAvg.getAsDouble() : 0; + float smoothedAbsoluteElevation = currentAvg.isPresent() ? (float) currentAvg.getAsDouble() : absoluteElevation; + float smoothedRelativeElevation = smoothedAbsoluteElevation - startElevation; + float deltaFromCurrentFloor = getDeltaFromCurrentFloor(smoothedAbsoluteElevation); + int detectedFloor = detectFloorFromAbsoluteElevation(smoothedAbsoluteElevation, smoothedRelativeElevation); + boolean bootstrapInitialFloorByAnchors = hasFloorAltitudeAnchors() && !initialAnchorFloorResolved; + boolean strongVerticalProgress = Math.abs(deltaFromCurrentFloor) + >= getStrongProgressThreshold(this.currentFloor, detectedFloor); + boolean floorAllowed = bootstrapInitialFloorByAnchors || isFloorChangeAllowed(strongVerticalProgress); + boolean farEnoughFromCurrentFloor = Math.abs(deltaFromCurrentFloor) + >= getFloorSwitchEnterThreshold(this.currentFloor, detectedFloor); + boolean floorJumpReasonable = bootstrapInitialFloorByAnchors + || Math.abs(detectedFloor - this.currentFloor) <= MAX_SINGLE_FLOOR_JUMP; + int requiredConfirmations = bootstrapInitialFloorByAnchors ? 1 : FLOOR_SWITCH_CONFIRMATIONS; + + if (detectedFloor != this.currentFloor && farEnoughFromCurrentFloor && floorJumpReasonable && floorAllowed) { + if (pendingFloorCandidate == detectedFloor) { + pendingFloorCandidateCount++; + } else { + pendingFloorCandidate = detectedFloor; + pendingFloorCandidateCount = 1; + } + + if (pendingFloorCandidateCount >= requiredConfirmations) { + this.currentFloor = detectedFloor; + pendingFloorCandidate = Integer.MIN_VALUE; + pendingFloorCandidateCount = 0; + if (bootstrapInitialFloorByAnchors) { + initialAnchorFloorResolved = true; + } + Log.d("PdrProcessing", "Floor changed to " + this.currentFloor + " (barometer elevation " + smoothedRelativeElevation + "m)"); + } + } else { + pendingFloorCandidate = Integer.MIN_VALUE; + pendingFloorCandidateCount = 0; + if (bootstrapInitialFloorByAnchors && detectedFloor == this.currentFloor) { + initialAnchorFloorResolved = true; + } + } - // Check if we moved floor by comparing with start position - if(Math.abs(finishAvg - startElevation) > this.floorHeight) { - // Change floors - 'floor' division - this.currentFloor += (finishAvg - startElevation)/this.floorHeight; + if (hasFloorAltitudeAnchors()) { + float floorAnchor = getAbsoluteFloorAnchor(this.currentFloor); + if (!Float.isNaN(floorAnchor)) { + this.startElevation = this.startElevation + + FLOOR_BASELINE_RECENTER_RATIO * (smoothedAbsoluteElevation - floorAnchor); + } + } else if (!farEnoughFromCurrentFloor) { + float baselineTarget = smoothedAbsoluteElevation - this.currentFloor * this.floorHeight; + this.startElevation = this.startElevation + + FLOOR_BASELINE_RECENTER_RATIO * (baselineTarget - this.startElevation); } + + this.lastSmoothedRelativeElevation = smoothedRelativeElevation; + this.elevation = smoothedAbsoluteElevation - this.startElevation; + return this.elevation; } + // Return current elevation - return elevation; + this.elevation = absoluteElevation - startElevation; + return this.elevation; } // Keep elevation at zero if there is no calculated value return 0; } - /** - * Uses the Weiberg Stride Length formula to calculate step length from accelerometer values. - * - * @param accelMagnitude magnitude of acceleration values between the last and current step. - * @return float stride length in meters. - */ - private float weibergMinMax(List accelMagnitude) { + // Uses the Weiberg Stride Length formula to calculate step length from accelerometer values. + // Uses a step-frequency classifier to scale stride length. +// Parameter accelMagnitude: magnitude of acceleration values between the last and current step. +// Parameter stepDurationMs: duration of the current step in milliseconds. +// Returns: float stride length in meters. + private float weibergMinMax(List accelMagnitude, long stepDurationMs) { // if the list itself is null or empty, return 0 (or return other default values as needed) if (accelMagnitude == null || accelMagnitude.isEmpty()) { return 0f; @@ -252,56 +512,493 @@ private float weibergMinMax(List accelMagnitude) { // calculate bounce float bounce = (float) Math.pow((maxAccel - minAccel), 0.25); + float weibergK = K; // determine which constant to use based on settings if (this.settings.getBoolean("overwrite_constants", false)) { - return bounce * Float.parseFloat(settings.getString("weiberg_k", "0.934")) * 2; + weibergK = Float.parseFloat(settings.getString("weiberg_k", "0.934")); } - return bounce * K * 2; + // Base Weiberg calculation + float baseStepLength = bounce * weibergK * 2; + + if (stepDurationMs <= 0 || stepDurationMs >= 2000) { + return baseStepLength; + } + + float stepFreq = 1000f / stepDurationMs; // Hz + + // Scale stride by walking cadence class. + float frequencyFactor; + if (stepFreq < 1.2f) { + // Slow walking + frequencyFactor = 0.85f; + } else if (stepFreq < 1.8f) { + // Normal walking + frequencyFactor = 1.0f; + } else if (stepFreq < 2.2f) { + // Fast walking + frequencyFactor = 1.1f; + } else { + // Very fast (jogging) + frequencyFactor = 1.2f; + } + + Log.d("PdrProcessing", String.format("Step: freq=%.2fHz factor=%.2f len=%.3fm", + stepFreq, frequencyFactor, baseStepLength * frequencyFactor)); + + return baseStepLength * frequencyFactor; } - /** - * Get the current X and Y coordinates from the PDR processing class. - * The coordinates are in meters, the start of the recording is the (0,0) - * - * @return float array of size 2, with the X and Y coordinates respectively. - */ + // Get the current X and Y coordinates from the PDR processing class. + // The coordinates are in meters, the start of the recording is the (0,0) +// Returns: float array of size 2, with the X and Y coordinates respectively. public float[] getPDRMovement() { float [] pdrPosition= new float[] {positionX,positionY}; return pdrPosition; } - /** - * Get the current elevation as calculated by the PDR class. - * - * @return current elevation in meters, relative to the start position. - */ + // Get the current elevation as calculated by the PDR class. +// Returns: current elevation in meters, relative to the start position. public float getCurrentElevation() { return this.elevation; } - /** - * Get the current floor number as estimated by the PDR class. - * - * @return current floor number, assuming start position is on level zero. - */ + // Get the current floor number as estimated by the PDR class. +// Returns: current floor number, assuming start position is on level zero. public int getCurrentFloor() { return this.currentFloor; } - /** - * Estimates if the user is currently taking an elevator. - * From the gravity and gravity-removed acceleration values the magnitude of horizontal and - * vertical acceleration is calculated and stored over time. Averaging these values and - * comparing with the thresholds set for this class, it estimates if the current movement - * matches what is expected from an elevator ride. - * - * @param gravity array of size three, strength of gravity along the phone's x-y-z axis. - * @param acc array of size three, acceleration other than gravity detected by the phone. - * @return boolean true if currently in an elevator, false otherwise. - */ + public void setIndoorFeatureZones(List> stairsZones, List> liftZones, List> walls) { + this.indoorStairsZones = stairsZones != null ? new java.util.ArrayList<>(stairsZones) : new java.util.ArrayList<>(); + this.indoorLiftZones = liftZones != null ? new java.util.ArrayList<>(liftZones) : new java.util.ArrayList<>(); + this.indoorWalls = walls != null ? new java.util.ArrayList<>(walls) : new java.util.ArrayList<>(); + } + + public void configureFloorReference(float floorHeightMeters, int floorIndex, float[] floorAltitudeAnchorsMeters) { + if (!Float.isNaN(floorHeightMeters) && floorHeightMeters > 1.0f) { + this.floorHeight = floorHeightMeters; + } + this.currentFloor = floorIndex; + this.floorAltitudeAnchorsMeters = floorAltitudeAnchorsMeters != null + ? java.util.Arrays.copyOf(floorAltitudeAnchorsMeters, floorAltitudeAnchorsMeters.length) + : null; + this.pendingFloorCandidate = Integer.MIN_VALUE; + this.pendingFloorCandidateCount = 0; + this.initialAnchorFloorResolved = false; + } + + public void setMotionState(boolean stationary, float speedMetersPerSecond, float motionMagnitude) { + // Compatibility hook for SensorFusion; keeps floor gating/motion hints in sync. + if (!stationary && (speedMetersPerSecond > 0.55f || motionMagnitude > 0.22f)) { + this.isStairsMotion = true; + } else if (stationary) { + this.isStairsMotion = false; + } + } + + private List initializationCandidates = new ArrayList<>(); + private static final int REQUIRED_INITIALIZATION_SAMPLES = 3; + + // Initialise fusion with a starting GNSS or WiFi location in latitude/longitude. + // Modified to use average of early samples for more robust initialization. + public void initializeWithLocation(double latitude, double longitude) { + if (!this.fusionInitialized) { + initializationCandidates.add(new LatLng(latitude, longitude)); + + if (initializationCandidates.size() >= REQUIRED_INITIALIZATION_SAMPLES) { + // Average the samples for better precision + double sumLat = 0; + double sumLon = 0; + for (LatLng candidate : initializationCandidates) { + sumLat += candidate.latitude; + sumLon += candidate.longitude; + } + this.originLat = sumLat / initializationCandidates.size(); + this.originLon = sumLon / initializationCandidates.size(); + + double[] local = latLonToLocal(this.originLat, this.originLon); + this.fusedX = local[0]; + this.fusedY = local[1]; + + this.positionX = 0f; + this.positionY = 0f; + + // Reduced initial covariance for faster convergence. + this.P = new double[][]{{1.0, 0}, {0, 1.0}}; + this.fusionInitialized = true; + initializationCandidates.clear(); // clean up + } + } + } + + private double[] latLonToLocal(double latitude, double longitude) { + double dx = (longitude - this.originLon) * 111111.0 * Math.cos(Math.toRadians(this.originLat)); + double dy = (latitude - this.originLat) * 111111.0; + return new double[]{dx, dy}; + } + + private LatLng localToLatLon(double x, double y) { + double lat = this.originLat + (y / 111111.0); + double lon = this.originLon + (x / (111111.0 * Math.cos(Math.toRadians(this.originLat)))); + return new LatLng(lat, lon); + } + + private void applyMapMatching(double prevX, double prevY) { + LatLng fusedLatLon = localToLatLon(this.fusedX, this.fusedY); + if (!BuildingPolygon.inAnyKnownBuilding(fusedLatLon)) { + // Keep previous position to avoid walking through walls/outside building area + this.fusedX = prevX; + this.fusedY = prevY; + return; + } + + // Check if movement crossed any indoor wall + if (indoorWalls != null && !indoorWalls.isEmpty()) { + LatLng prevLatLon = localToLatLon(prevX, prevY); + if (GeometryUtils.crossesWall(prevLatLon, fusedLatLon, indoorWalls)) { + Log.d("PdrProcessing", "EKF Update blocked by indoor wall collision"); + this.fusedX = prevX; + this.fusedY = prevY; + return; + } + } + + // Additional proximity-based behavior: don't allow spontaneous floor change unless near stairs/lift + if (!isNearStairsOrLift() && this.currentFloor != getFloorFromBarometer()) { + // revert to previous floor if map policy forbids unstructured floor jump + Log.d("PdrProcessing", "Floor change blocked, not near stairs/lift"); + this.currentFloor = getFloorFromBarometer(); + } + } + + private void predictFusion(double dx, double dy) { + double prevX = this.fusedX; + double prevY = this.fusedY; + + // Process model: fused position shifts by PDR delta + this.fusedX += dx; + this.fusedY += dy; + + // Covariance update + P[0][0] += Q[0][0]; + P[0][1] += Q[0][1]; + P[1][0] += Q[1][0]; + P[1][1] += Q[1][1]; + + applyMapMatching(prevX, prevY); + } + + private void updateWithMeasurement(double measX, double measY, double[][] R) { + if (!fusionInitialized || P == null) { + return; + } + + double y0 = measX - this.fusedX; + double y1 = measY - this.fusedY; + + double s00 = P[0][0] + R[0][0]; + double s01 = P[0][1] + R[0][1]; + double s10 = P[1][0] + R[1][0]; + double s11 = P[1][1] + R[1][1]; + + double detS = s00 * s11 - s01 * s10; + if (Math.abs(detS) < 1e-9) return; + + double invS00 = s11 / detS; + double invS01 = -s01 / detS; + double invS10 = -s10 / detS; + double invS11 = s00 / detS; + + // Kalman gain K = P S^{-1} + double k00 = P[0][0] * invS00 + P[0][1] * invS10; + double k01 = P[0][0] * invS01 + P[0][1] * invS11; + double k10 = P[1][0] * invS00 + P[1][1] * invS10; + double k11 = P[1][0] * invS01 + P[1][1] * invS11; + + double prevX = this.fusedX; + double prevY = this.fusedY; + + // State update + this.fusedX += k00 * y0 + k01 * y1; + this.fusedY += k10 * y0 + k11 * y1; + + // Covariance update + double i00 = 1 - k00; + double i01 = -k01; + double i10 = -k10; + double i11 = 1 - k11; + double newP00 = i00 * P[0][0] + i01 * P[1][0]; + double newP01 = i00 * P[0][1] + i01 * P[1][1]; + double newP10 = i10 * P[0][0] + i11 * P[1][0]; + double newP11 = i10 * P[0][1] + i11 * P[1][1]; + P[0][0] = newP00; + P[0][1] = newP01; + P[1][0] = newP10; + P[1][1] = newP11; + + applyMapMatching(prevX, prevY); + } + + // Process a GNSS measurement - Light trajectory correction WITHOUT affecting heading direction. + // GNSS is unreliable indoors but can provide light position hints. + // Heading is NEVER affected - only XY position gets minimal adjustment. + public void processGnssLocation(double latitude, double longitude, float accuracy) { + LatLng gnssPoint = new LatLng(latitude, longitude); + + if (!fusionInitialized) { + // Save GNSS position to history for reference only + gnssHistoryBuffer.offer(gnssPoint); + if (gnssHistoryBuffer.size() > MAX_COORDINATE_HISTORY) { + gnssHistoryBuffer.poll(); + } + return; + } + + double[] local = latLonToLocal(latitude, longitude); + + // Calculate distance error between GNSS and current fused position + double errorDistance = Math.sqrt( + Math.pow(local[0] - this.fusedX, 2) + + Math.pow(local[1] - this.fusedY, 2) + ); + + // Apply only tiny position nudge with stricter GNSS gates. + if (errorDistance > GNSS_ERROR_THRESHOLD_METERS) { + Log.d("PdrProcessing", "GNSS rejected: error " + errorDistance + "m > threshold " + GNSS_ERROR_THRESHOLD_METERS + "m"); + stableGnssCandidateCount = 0; + } else if (accuracy > GNSS_MIN_ACCURACY_METERS) { + stableGnssCandidateCount = 0; + } else if (accuracy <= GNSS_MIN_ACCURACY_METERS) { + if (lastStableGnssCandidate != null + && getDistanceMeters(lastStableGnssCandidate, gnssPoint) <= GNSS_STABLE_RADIUS_METERS) { + stableGnssCandidateCount++; + } else { + stableGnssCandidateCount = 1; + } + lastStableGnssCandidate = gnssPoint; + + if (stableGnssCandidateCount >= GNSS_STABLE_CONFIRMATIONS_REQUIRED) { + double correctionFactor = GNSS_POSITION_NUDGE_FACTOR; + double errorX = local[0] - this.fusedX; + double errorY = local[1] - this.fusedY; + + applyBoundedCorrection(errorX, errorY, correctionFactor, GNSS_MAX_CORRECTION_PER_UPDATE_METERS); + + Log.d("PdrProcessing", String.format( + "GNSS gated correction: %.2fm error, stable %d times, applied %.2f%% adjustment", + errorDistance, GNSS_STABLE_CONFIRMATIONS_REQUIRED, correctionFactor * 100)); + + // Apply only once per stable sequence. + stableGnssCandidateCount = 0; + } else { + Log.d("PdrProcessing", "GNSS waiting stable confirmations: " + + stableGnssCandidateCount + "/" + GNSS_STABLE_CONFIRMATIONS_REQUIRED); + } + } + + // Save GNSS position to history for reference + gnssHistoryBuffer.offer(gnssPoint); + if (gnssHistoryBuffer.size() > MAX_COORDINATE_HISTORY) { + gnssHistoryBuffer.poll(); + } + } + + // Process a WiFi measurement - Light trajectory correction WITHOUT affecting heading direction. + // WiFi is used primarily for floor level detection. + // Position can receive light corrections but heading is NEVER affected. + public void processWifiLocation(LatLng wifiLocation, int floor) { + if (wifiLocation == null) { + return; + } + + // Apply light WiFi position correction and floor detection. + Log.d("PdrProcessing", String.format("WiFi received: lat=%.6f, lon=%.6f, floor=%d", + wifiLocation.latitude, wifiLocation.longitude, floor)); + + if (fusionInitialized) { + double[] local = latLonToLocal(wifiLocation.latitude, wifiLocation.longitude); + + // Calculate distance error between WiFi and current fused position + double errorDistance = Math.sqrt( + Math.pow(local[0] - this.fusedX, 2) + + Math.pow(local[1] - this.fusedY, 2) + ); + + // Keep WiFi pull extremely small to satisfy assignment without distorting direction. + if (errorDistance <= WIFI_ERROR_THRESHOLD_METERS && WIFI_POSITION_NUDGE_FACTOR > 0.0) { + double correctionFactor = WIFI_POSITION_NUDGE_FACTOR; + double errorX = local[0] - this.fusedX; + double errorY = local[1] - this.fusedY; + + applyBoundedCorrection(errorX, errorY, correctionFactor, WIFI_MAX_CORRECTION_PER_UPDATE_METERS); + + Log.d("PdrProcessing", String.format( + "WiFi ultra-light correction: %.2fm error, applied %.2f%% position adjustment (heading protected)", + errorDistance, correctionFactor * 100)); + } else { + Log.d("PdrProcessing", "WiFi correction skipped (distance gate or nudge factor <= 0)"); + } + } + + // Floor level detection - independent of position + if (Math.abs(floor - this.currentFloor) > 0) { + // Only change floor if we're in known stair/elevator zones + boolean inStairsOrLift = isNearStairsOrLift() || this.isElevatorMotion || this.isStairsMotion; + if (inStairsOrLift) { + this.currentFloor = floor; + Log.d("PdrProcessing", "WiFi floor change applied: " + floor + " (near stairs/lift)"); + } else { + Log.d("PdrProcessing", "WiFi floor ignored (not near stairs/lift)"); + } + } + + // Save WiFi position to history for reference + wifiHistoryBuffer.offer(wifiLocation); + if (wifiHistoryBuffer.size() > MAX_COORDINATE_HISTORY) { + wifiHistoryBuffer.poll(); + } + } + + public float[] getFusedPosition() { + return new float[]{(float)this.fusedX, (float)this.fusedY}; + } + + public LatLng getFusedLatLon() { + return localToLatLon(this.fusedX, this.fusedY); + } + + private void applyBoundedCorrection(double errorX, double errorY, double correctionFactor, double maxCorrectionMeters) { + double proposedX = errorX * correctionFactor; + double proposedY = errorY * correctionFactor; + double magnitude = Math.hypot(proposedX, proposedY); + + if (magnitude > maxCorrectionMeters && magnitude > 1e-6) { + double scale = maxCorrectionMeters / magnitude; + proposedX *= scale; + proposedY *= scale; + } + + this.fusedX += proposedX; + this.fusedY += proposedY; + } + + // Coordinate history getters + // Get the history of GNSS verified coordinates (last up to 7 positions) +// Returns: List of LatLng points representing historical GNSS positions + public List getGnssHistoryBuffer() { + return new ArrayList<>(gnssHistoryBuffer); + } + + // Get the history of WiFi verified coordinates (last up to 7 positions) +// Returns: List of LatLng points representing historical WiFi positions + public List getWifiHistoryBuffer() { + return new ArrayList<>(wifiHistoryBuffer); + } + + // Get the history of PDR calculated coordinates (last up to 7 positions) +// Returns: List of LatLng points representing historical PDR positions + public List getPdrHistoryBuffer() { + return new ArrayList<>(pdrHistoryBuffer); + } + + // Get all coordinate histories in a combined format for display/logging +// Returns: Map containing GNSS, WiFi, and PDR history buffers + public java.util.Map> getAllCoordinateHistories() { + java.util.Map> histories = new java.util.HashMap<>(); + histories.put("GNSS", new ArrayList<>(gnssHistoryBuffer)); + histories.put("WiFi", new ArrayList<>(wifiHistoryBuffer)); + histories.put("PDR", new ArrayList<>(pdrHistoryBuffer)); + return histories; + } + + // Clear all coordinate history buffers (useful for resetting tracking) + public void clearCoordinateHistories() { + gnssHistoryBuffer.clear(); + wifiHistoryBuffer.clear(); + pdrHistoryBuffer.clear(); + Log.d("PdrProcessing", "Coordinate history buffers cleared"); + } + + public int getFloorFromBarometer() { + float absoluteElevation = this.startElevation + this.elevation; + return detectFloorFromAbsoluteElevation(absoluteElevation, this.elevation); + } + + private boolean isNearZone(LatLng current, LatLng[] zone, float radiusMeters) { + if (current == null) return false; + for (LatLng point : zone) { + if (getDistanceMeters(current, point) <= radiusMeters) { + return true; + } + } + return false; + } + + private boolean isNearStairsOrLift() { + LatLng fusedLatLon = getFusedLatLon(); + return isNearZone(fusedLatLon, STAIRS_ZONES, MAP_ZONE_RADIUS_METERS) + || isNearZone(fusedLatLon, LIFT_ZONES, MAP_ZONE_RADIUS_METERS); + } + + private boolean isNearMappedZone(LatLng current, List> zones) { + if (current == null || zones == null || zones.isEmpty()) { + return false; + } + for (List zone : zones) { + if (GeometryUtils.isPointNearFeature(current, zone, MAP_ZONE_RADIUS_METERS)) { + return true; + } + } + return false; + } + + private boolean hasMappedVerticalZones() { + return (indoorStairsZones != null && !indoorStairsZones.isEmpty()) + || (indoorLiftZones != null && !indoorLiftZones.isEmpty()); + } + + private boolean isFloorChangeAllowed(boolean strongVerticalProgress) { + LatLng fusedLatLon = getFusedLatLon(); + if (hasMappedVerticalZones()) { + boolean nearStairs = isNearMappedZone(fusedLatLon, indoorStairsZones); + boolean nearLift = isNearMappedZone(fusedLatLon, indoorLiftZones); + return (nearStairs && (isStairsMotion || strongVerticalProgress)) + || (nearLift && (isElevatorMotion || strongVerticalProgress)); + } + return isNearStairsOrLift() || isElevatorMotion || isStairsMotion || strongVerticalProgress; + } + + private float getDistanceMeters(LatLng a, LatLng b) { + double lat1 = Math.toRadians(a.latitude); + double lon1 = Math.toRadians(a.longitude); + double lat2 = Math.toRadians(b.latitude); + double lon2 = Math.toRadians(b.longitude); + + double dLat = lat2 - lat1; + double dLon = lon2 - lon1; + double r = 6371000; // Earth radius in meters + + double s = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) * Math.sin(dLon / 2); + double c = 2 * Math.atan2(Math.sqrt(s), Math.sqrt(1 - s)); + return (float) (r * c); + } + + public boolean isFusionInitialized() { + return fusionInitialized; + } + + // Estimates if the user is currently taking an elevator. + // From the gravity and gravity-removed acceleration values the magnitude of horizontal and + // vertical acceleration is calculated and stored over time. Averaging these values and + // comparing with the thresholds set for this class, it estimates if the current movement + // matches what is expected from an elevator ride. +// Parameter gravity: array of size three, strength of gravity along the phone's x-y-z axis. +// Parameter acc: array of size three, acceleration other than gravity detected by the phone. +// Returns: boolean true if currently in an elevator, false otherwise. public boolean estimateElevator(float[] gravity, float[] acc) { // Standard gravity float g = SensorManager.STANDARD_GRAVITY; @@ -331,30 +1028,32 @@ public boolean estimateElevator(float[] gravity, float[] acc) { OptionalDouble optHorizontalAvg = horizontalMemory.stream().mapToDouble(Math::abs).average(); float horizontalAvg = optHorizontalAvg.isPresent() ? (float) optHorizontalAvg.getAsDouble() : 0; - //System.err.println("LIFT: Vertical: " + verticalAvg); - //System.err.println("LIFT: Horizontal: " + horizontalAvg); + // System.err.println("LIFT: Vertical: " + verticalAvg); + // System.err.println("LIFT: Horizontal: " + horizontalAvg); if(this.settings.getBoolean("overwrite_constants", false)) { float eps = Float.parseFloat(settings.getString("epsilon", "0.18")); return horizontalAvg < eps && verticalAvg > movementThreshold; } // Check if there is minimal horizontal and significant vertical movement - return horizontalAvg < epsilon && verticalAvg > movementThreshold; + boolean elevatorCandidate = horizontalAvg < epsilon && verticalAvg > movementThreshold; + this.isElevatorMotion = elevatorCandidate; + return elevatorCandidate; } + + this.isElevatorMotion = false; return false; } - /** - * Resets all values stored in the PDR function and re-initialises all buffers. - * Used to reset to zero position and remove existing history. - */ + // Resets all values stored in the PDR function and re-initialises all buffers. + // Used to reset to zero position and remove existing history. public void resetPDR() { // Check if estimate or manual values should be used this.useManualStep = this.settings.getBoolean("manual_step_values", false); if(useManualStep) { try { - // Retrieve manual step length + // Retrieve manual step length this.stepLength = this.settings.getInt("user_step_length", 75) / 100f; } catch (Exception e) { // Invalid values - reset to defaults @@ -395,23 +1094,102 @@ public void resetPDR() { this.startElevationBuffer = new Float[3]; // Start floor - assumed to be zero this.currentFloor = 0; + this.pendingFloorCandidate = Integer.MIN_VALUE; + this.pendingFloorCandidateCount = 0; + this.initialAnchorFloorResolved = false; + this.lastSmoothedRelativeElevation = 0f; + this.enableMagneticCompensation = this.settings.getBoolean("enable_magnetic_compensation", true); + if (this.magneticCompensation != null) { + this.magneticCompensation.setEnabled(this.enableMagneticCompensation); + } } - /** - * Getter for the average step length calculated from the aggregated distance and step count. - * - * @return average step length in meters. - */ + public void setMagneticCompensationEnabled(boolean enabled) { + this.enableMagneticCompensation = enabled; + if (this.magneticCompensation != null) { + this.magneticCompensation.setEnabled(enabled); + } + } + + public boolean isMagneticCompensationEnabled() { + return this.enableMagneticCompensation; + } + + private boolean hasFloorAltitudeAnchors() { + return floorAltitudeAnchorsMeters != null && floorAltitudeAnchorsMeters.length > 0; + } + + private float getAbsoluteFloorAnchor(int floorIndex) { + if (!hasFloorAltitudeAnchors()) { + return Float.NaN; + } + if (floorIndex < 0 || floorIndex >= floorAltitudeAnchorsMeters.length) { + return Float.NaN; + } + return floorAltitudeAnchorsMeters[floorIndex]; + } + + private int detectFloorFromAbsoluteElevation(float absoluteElevation, float relativeElevation) { + if (hasFloorAltitudeAnchors()) { + int bestFloor = this.currentFloor; + float bestDistance = Float.MAX_VALUE; + for (int i = 0; i < floorAltitudeAnchorsMeters.length; i++) { + float anchor = floorAltitudeAnchorsMeters[i]; + if (Float.isNaN(anchor)) continue; + float d = Math.abs(absoluteElevation - anchor); + if (d < bestDistance) { + bestDistance = d; + bestFloor = i; + } + } + return bestFloor; + } + return Math.round(relativeElevation / this.floorHeight); + } + + private float getDeltaFromCurrentFloor(float absoluteElevation) { + float anchor = getAbsoluteFloorAnchor(this.currentFloor); + if (!Float.isNaN(anchor)) { + return absoluteElevation - anchor; + } + return absoluteElevation - (this.startElevation + this.currentFloor * this.floorHeight); + } + + private float getFloorSpanMeters(int fromFloor, int toFloor) { + if (hasFloorAltitudeAnchors()) { + float anchorA = getAbsoluteFloorAnchor(fromFloor); + float anchorB = getAbsoluteFloorAnchor(toFloor); + if (!Float.isNaN(anchorA) && !Float.isNaN(anchorB)) { + return Math.max(1.5f, Math.abs(anchorB - anchorA)); + } + } + return Math.max(1.5f, this.floorHeight); + } + + private float getFloorSwitchEnterThreshold(int fromFloor, int toFloor) { + float ratio = hasFloorAltitudeAnchors() ? ANCHORED_FLOOR_SWITCH_ENTER_RATIO : FLOOR_SWITCH_ENTER_RATIO; + return getFloorSpanMeters(fromFloor, toFloor) * ratio; + } + + private float getStrongProgressThreshold(int fromFloor, int toFloor) { + float ratio = hasFloorAltitudeAnchors() ? ANCHORED_FLOOR_SWITCH_STRONG_PROGRESS_RATIO : FLOOR_SWITCH_STRONG_PROGRESS_RATIO; + return getFloorSpanMeters(fromFloor, toFloor) * ratio; + } + + // Getter for the average step length calculated from the aggregated distance and step count. +// Returns: average step length in meters. public float getAverageStepLength(){ - //Calculate average step length + // Calculate average step length float averageStepLength = sumStepLength/(float) stepCount; - //Reset sum and number of steps + // Reset sum and number of steps stepCount = 0; sumStepLength = 0; - //Return average step length + // Return average step length return averageStepLength; } } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/PermissionManager.java b/app/src/main/java/com/openpositioning/PositionMe/utils/PermissionManager.java index 40c937ab..735e98e9 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/utils/PermissionManager.java +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/PermissionManager.java @@ -17,26 +17,22 @@ import java.util.ArrayList; import java.util.List; -/** - * A helper class responsible for checking and requesting all dangerous permissions - * that the application needs in order to function. - * - * This class: - * - Manages the permissions list. - * - Checks if all permissions are granted. - * - Requests missing permissions. - * - Handles both the first-time and permanent denial scenarios. - * - * Usage from MainActivity: - * PermissionManager permissionManager = new PermissionManager(MainActivity.this, new PermissionManager.PermissionCallback() { - * @Override - * public void onAllPermissionsGranted() { - * // e.g. call allPermissionsObtained() in MainActivity - * allPermissionsObtained(); - * } - * }); - * permissionManager.checkAndRequestPermissions(); - */ +// A helper class responsible for checking and requesting all dangerous permissions +// that the application needs in order to function. +// This class: +// Manages the permissions list. +// Checks if all permissions are granted. +// Requests missing permissions. +// Handles both the first-time and permanent denial scenarios. +// Usage from MainActivity: +// PermissionManager permissionManager = new PermissionManager(MainActivity.this, new PermissionManager.PermissionCallback() { +// @Override +// public void onAllPermissionsGranted() { +// // e.g. call allPermissionsObtained() in MainActivity +// allPermissionsObtained(); +// } +// }); +// permissionManager.checkAndRequestPermissions(); public class PermissionManager { private static final int ALL_PERMISSIONS_REQUEST = 100; @@ -65,9 +61,7 @@ public PermissionManager(Activity activity, PermissionCallback callback) { requiredPermissions.add(Manifest.permission.ACTIVITY_RECOGNITION); } - /** - * Checks if all required permissions are already granted; if not, requests them. - */ + // Checks if all required permissions are already granted; if not, requests them. public void checkAndRequestPermissions() { if (!hasAllPermissions()) { ActivityCompat.requestPermissions( @@ -81,15 +75,12 @@ public void checkAndRequestPermissions() { } } - /** - * Must be called from the Activity's onRequestPermissionsResult: - * - * @Override - * public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - * super.onRequestPermissionsResult(requestCode, permissions, grantResults); - * permissionManager.handleRequestPermissionsResult(requestCode, permissions, grantResults); - * } - */ + // Must be called from the Activity's onRequestPermissionsResult: + // @Override + // public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + // super.onRequestPermissionsResult(requestCode, permissions, grantResults); + // permissionManager.handleRequestPermissionsResult(requestCode, permissions, grantResults); + // } public void handleRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { @@ -126,9 +117,7 @@ public void handleRequestPermissionsResult(int requestCode, } } - /** - * Checks if the app has all the required permissions granted. - */ + // Checks if the app has all the required permissions granted. private boolean hasAllPermissions() { for (String perm : requiredPermissions) { if (ContextCompat.checkSelfPermission(activity, perm) != PackageManager.PERMISSION_GRANTED) { @@ -138,9 +127,7 @@ private boolean hasAllPermissions() { return true; } - /** - * Shows an AlertDialog if the user has denied permissions for the first time. - */ + // Shows an AlertDialog if the user has denied permissions for the first time. private void showFirstDenialDialog() { if (!activity.isFinishing()) { new AlertDialog.Builder(activity) @@ -154,9 +141,7 @@ private void showFirstDenialDialog() { } } - /** - * Shows an AlertDialog if the user has permanently denied the permissions. - */ + // Shows an AlertDialog if the user has permanently denied the permissions. private void showPermanentDenialDialog() { if (!activity.isFinishing()) { new AlertDialog.Builder(activity) @@ -175,10 +160,10 @@ private void showPermanentDenialDialog() { } } - /** - * Callback to notify the calling Activity when all permissions have been granted. - */ + // Callback to notify the calling Activity when all permissions have been granted. public interface PermissionCallback { void onAllPermissionsGranted(); } } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/TrajectoryVerifier.java b/app/src/main/java/com/openpositioning/PositionMe/utils/TrajectoryVerifier.java new file mode 100644 index 00000000..cba9dac6 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/TrajectoryVerifier.java @@ -0,0 +1,158 @@ +package com.openpositioning.PositionMe.utils; + +import android.util.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import com.openpositioning.PositionMe.Traj; +// Validates recorded trajectory files before replay. +// This helper logs structural information and data availability so replay +// issues can be diagnosed quickly from Logcat. +public class TrajectoryVerifier { + private static final String TAG = "TrajectoryVerifier"; + + // Verifies that a trajectory file exists, is readable, and contains usable + // movement data for replay. The method logs parsed metadata and source-data + // coverage to simplify troubleshooting. + // Parameter filePath: Absolute path to a serialized trajectory file. + // Returns: True when at least GNSS or PDR samples are present. + public static boolean verifyTrajectoryFile(String filePath) { + File file = new File(filePath); + + Log.d(TAG, "Trajectory file verification started"); + Log.d(TAG, "File: " + filePath); + + if (!file.exists()) { + Log.e(TAG, "ERROR: File does not exist!"); + return false; + } + + if (!file.canRead()) { + Log.e(TAG, "ERROR: File exists but cannot be read!"); + return false; + } + + Log.d(TAG, "File size: " + file.length() + " bytes"); + + if (file.length() == 0) { + Log.e(TAG, "ERROR: File is empty (0 bytes)!"); + return false; + } + + try (FileInputStream fis = new FileInputStream(file)) { + Traj.Trajectory traj = Traj.Trajectory.parseFrom(fis); + + Log.d(TAG, "Parsed data summary"); + Log.d(TAG, "Trajectory ID: " + traj.getTrajectoryId()); + Log.d(TAG, "Start Timestamp: " + traj.getStartTimestamp()); + Log.d(TAG, "Android Version: " + traj.getAndroidVersion()); + + if (traj.hasInitialPosition()) { + Traj.GNSSPosition initPos = traj.getInitialPosition(); + Log.d(TAG, "Initial Position: lat=" + initPos.getLatitude() + + ", lon=" + initPos.getLongitude()); + } + + Log.d(TAG, "Data counts"); + Log.d(TAG, "IMU Data: " + traj.getImuDataCount()); + Log.d(TAG, "GNSS Data: " + traj.getGnssDataCount()); + Log.d(TAG, "PDR Data: " + traj.getPdrDataCount()); + Log.d(TAG, "Magnetometer Data: " + traj.getMagnetometerDataCount()); + Log.d(TAG, "Pressure Data: " + traj.getPressureDataCount()); + Log.d(TAG, "WiFi Fingerprints: " + traj.getWifiFingerprintsCount()); + Log.d(TAG, "BLE Data: " + traj.getBleDataCount()); + Log.d(TAG, "Test Points: " + traj.getTestPointsCount()); + + boolean hasGnss = traj.getGnssDataCount() > 0; + boolean hasPdr = traj.getPdrDataCount() > 0; + + Log.d(TAG, "GNSS data details"); + if (hasGnss) { + Log.d(TAG, "GNSS data present: " + traj.getGnssDataCount() + " points"); + + // Log first and last GNSS samples for sanity checks. + Traj.GNSSReading first = traj.getGnssData(0); + Traj.GNSSReading last = traj.getGnssData(traj.getGnssDataCount() - 1); + + Log.d(TAG, "First GNSS point:"); + Log.d(TAG, " Lat: " + first.getPosition().getLatitude()); + Log.d(TAG, " Lon: " + first.getPosition().getLongitude()); + Log.d(TAG, " Accuracy: " + first.getAccuracy() + "m"); + Log.d(TAG, " Speed: " + first.getSpeed() + " m/s"); + Log.d(TAG, " Bearing: " + first.getBearing() + " deg"); + + Log.d(TAG, "Last GNSS point:"); + Log.d(TAG, " Lat: " + last.getPosition().getLatitude()); + Log.d(TAG, " Lon: " + last.getPosition().getLongitude()); + Log.d(TAG, " Accuracy: " + last.getAccuracy() + "m"); + + // Estimate path extent from first and last GNSS samples. + double distance = calculateDistance( + first.getPosition().getLatitude(), + first.getPosition().getLongitude(), + last.getPosition().getLatitude(), + last.getPosition().getLongitude() + ); + Log.d(TAG, "Distance first->last: " + String.format("%.1f", distance) + "m"); + } else { + Log.e(TAG, "No GNSS data in file"); + } + + Log.d(TAG, "PDR data details"); + if (hasPdr) { + Log.d(TAG, "PDR data present: " + traj.getPdrDataCount() + " points"); + + Traj.RelativePosition firstPdr = traj.getPdrData(0); + Traj.RelativePosition lastPdr = traj.getPdrData(traj.getPdrDataCount() - 1); + + Log.d(TAG, "First PDR: x=" + firstPdr.getX() + ", y=" + firstPdr.getY()); + Log.d(TAG, "Last PDR: x=" + lastPdr.getX() + ", y=" + lastPdr.getY()); + + double pdrDistance = Math.sqrt( + Math.pow(lastPdr.getX() - firstPdr.getX(), 2) + + Math.pow(lastPdr.getY() - firstPdr.getY(), 2) + ); + Log.d(TAG, "PDR distance: " + String.format("%.1f", pdrDistance) + "m"); + } else { + Log.e(TAG, "No PDR data in file"); + } + + Log.d(TAG, "Replay verdict"); + if (hasGnss && hasPdr) { + Log.d(TAG, "File is valid: contains both GNSS and PDR data"); + Log.d(TAG, "Trajectory should replay with full overlays"); + } else if (hasGnss) { + Log.w(TAG, "File contains GNSS only: replay will use GNSS path"); + } else if (hasPdr) { + Log.w(TAG, "File contains PDR only: replay will use PDR path"); + } else { + Log.e(TAG, "File is invalid for replay: no GNSS or PDR trajectory data"); + Log.e(TAG, "Recording may have failed or data was not persisted"); + } + + Log.d(TAG, "Trajectory file verification finished"); + + return hasGnss || hasPdr; + + } catch (IOException e) { + Log.e(TAG, "ERROR reading/parsing trajectory file", e); + Log.d(TAG, "Trajectory file verification finished with parser error"); + return false; + } + } + + // Computes great-circle distance between two latitude-longitude points. + private static double calculateDistance(double lat1, double lon1, double lat2, double lon2) { + final double R = 6371000; // Earth radius in meters + double dLat = Math.toRadians(lat2 - lat1); + double dLon = Math.toRadians(lon2 - lon1); + double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; + } +} + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/UtilFunctions.java b/app/src/main/java/com/openpositioning/PositionMe/utils/UtilFunctions.java index 56a88aa3..ee244e7d 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/utils/UtilFunctions.java +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/UtilFunctions.java @@ -10,32 +10,26 @@ import com.google.android.gms.maps.model.LatLng; import com.openpositioning.PositionMe.presentation.fragment.RecordingFragment; -/** - * Class containing utility functions which can used by other classes. - * @see RecordingFragment Currently used by RecordingFragment - */ +// Class containing utility functions which can used by other classes. +// Related: RecordingFragment Currently used by RecordingFragment public class UtilFunctions { // Constant 1degree of latitiude/longitude (in m) private static final int DEGREE_IN_M=111111; - /** - * Simple function to calculate the angle between two close points - * @param pointA Starting point - * @param pointB Ending point - * @return Angle between the points - */ + // Simple function to calculate the angle between two close points +// Parameter pointA: Starting point +// Parameter pointB: Ending point +// Returns: Angle between the points public static double calculateAngleSimple(LatLng pointA, LatLng pointB) { // Simple formula for close-by points return Math.toDegrees( Math.atan2(pointB.latitude-pointA.latitude, (pointB.longitude- pointA.longitude)*Math.cos(Math.toRadians(pointA.latitude)))); } - /** - * Calculate new coordinates based on net distance moved in PDR - * (as per WGS84 datum) - * @param initialLocation Current Location of user - * @param pdrMoved Amount of movement along X and Y - * @return new Coordinates based on the movement - */ + // Calculate new coordinates based on net distance moved in PDR + // (as per WGS84 datum) +// Parameter initialLocation: Current Location of user +// Parameter pdrMoved: Amount of movement along X and Y +// Returns: new Coordinates based on the movement public static LatLng calculateNewPos(LatLng initialLocation,float[] pdrMoved){ // Changes Euclidean movement into maps latitude and longitude as per WGS84 datum double newLatitude=initialLocation.latitude+(pdrMoved[1]/(DEGREE_IN_M)); @@ -44,44 +38,36 @@ public static LatLng calculateNewPos(LatLng initialLocation,float[] pdrMoved){ ); return new LatLng(newLatitude, newLongitude); } - /** - * Converts a degree value of Latitude into meters - * (as per WGS84 datum) - * @param degreeVal Value in degrees to convert to meters - * @return double corresponding to the value in meters. - */ + // Converts a degree value of Latitude into meters + // (as per WGS84 datum) +// Parameter degreeVal: Value in degrees to convert to meters +// Returns: double corresponding to the value in meters. public static double degreesToMetersLat(double degreeVal) { return degreeVal*DEGREE_IN_M; } - /** - * Converts a degree value of Longitude into meters - * (as per WGS84 datum) - * @param degreeVal Value in degrees to convert to meters - * @param latitude the latitude of the current position - * @return double corresponding to the value in meters. - */ + // Converts a degree value of Longitude into meters + // (as per WGS84 datum) +// Parameter degreeVal: Value in degrees to convert to meters +// Parameter latitude: the latitude of the current position +// Returns: double corresponding to the value in meters. public static double degreesToMetersLng(double degreeVal, double latitude) { return degreeVal*DEGREE_IN_M/Math.cos(Math.toRadians(latitude)); } - /** - * Calculates the distance between two LatLng points A and B (in meters) - * (Note: approximation: for short distances) - * @param pointA initial point - * @param pointB final point - * @return the distance between the two points - */ + // Calculates the distance between two LatLng points A and B (in meters) + // (Note: approximation: for short distances) +// Parameter pointA: initial point +// Parameter pointB: final point +// Returns: the distance between the two points public static double distanceBetweenPoints(LatLng pointA, LatLng pointB){ return Math.sqrt(Math.pow(degreesToMetersLat(pointA.latitude-pointB.latitude),2) + Math.pow(degreesToMetersLng(pointA.longitude-pointB.longitude,pointA.latitude),2)); } - /** - * Creates a bitmap from a vector - * @param context Context of activity being used - * @param vectorResourceID Resource id whose vector get converted to a Bitmap - * @return Bitmap of the resource vector - */ + // Creates a bitmap from a vector +// Parameter context: Context of activity being used +// Parameter vectorResourceID: Resource id whose vector get converted to a Bitmap +// Returns: Bitmap of the resource vector public static Bitmap getBitmapFromVector(Context context, int vectorResourceID) { // Get drawable vector Drawable vectorDrawable = ContextCompat.getDrawable(context, vectorResourceID); @@ -96,3 +82,5 @@ public static Bitmap getBitmapFromVector(Context context, int vectorResourceID) } } + + diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/WifiApObservation.java b/app/src/main/java/com/openpositioning/PositionMe/utils/WifiApObservation.java new file mode 100644 index 00000000..79053dff --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/WifiApObservation.java @@ -0,0 +1,14 @@ +package com.openpositioning.PositionMe.utils; + +public class WifiApObservation { + public final String bssid; // MAC + public final int rssi; // dBm + public final String ssid; // optional + + public WifiApObservation(String bssid, int rssi, String ssid) { + this.bssid = bssid; + this.rssi = rssi; + this.ssid = ssid; + } +} + diff --git a/app/src/main/proto/traj.proto b/app/src/main/proto/traj.proto index 95d8b0ac..d264e3fc 100644 --- a/app/src/main/proto/traj.proto +++ b/app/src/main/proto/traj.proto @@ -1,149 +1,244 @@ syntax = "proto3"; -message Trajectory { -string android_version = 1; -repeated Motion_Sample imu_data = 2; -repeated Pdr_Sample pdr_data = 3; -repeated Position_Sample position_data = 4; -repeated Pressure_Sample pressure_data = 5; -repeated Light_Sample light_data = 6; - -repeated GNSS_Sample gnss_data = 7; -repeated WiFi_Sample wifi_data = 8; -repeated AP_Data aps_data = 9; +option java_package = "com.openpositioning.PositionMe"; // Required +option java_outer_classname = "Traj"; -// UNIX timestamp (in milliseconds) recorded from the start of this -// trajectory data collection event. All future -// timestamps in sub classes are to be RELATIVE timestamps -// (in milliseconds) to this start time. -// E.g. -// start_timestamp = 1674819807315 (UTC 27 Jan 2023 in the morning) -// relative_timestamp = 3000 (3s) -int64 start_timestamp = 10; -string data_identifier = 11; -Sensor_Info accelerometer_info = 12; -Sensor_Info gyroscope_info = 13; -Sensor_Info rotation_vector_info = 14; -Sensor_Info magnetometer_info = 15; -Sensor_Info barometer_info = 16; -Sensor_Info light_sensor_info = 17; +message Trajectory { + string android_version = 1; + // version 2.0 + float trajectory_version = 2; + // trajectory id/name for identification + string trajectory_id = 3; + repeated IMUReading imu_data = 4; + repeated RelativePosition pdr_data = 5; + repeated MagnetometerReading magnetometer_data = 6; + repeated BarometerReading pressure_data = 7; + repeated LightReading light_data = 8; + repeated ProximityReading proximity_data = 9; + + repeated GNSSReading gnss_data = 10; + repeated Fingerprint wifi_fingerprints = 11; + repeated WiFiAPData aps_data = 12; + repeated WiFiRTTReading wifi_rtt_data = 13; + repeated Fingerprint ble_fingerprints = 14; + repeated BleData ble_data = 15; + + // UNIX timestamp (in milliseconds) recorded from the start of this + // trajectory data collection event. All future + // timestamps in sub classes are to be RELATIVE timestamps + // (in milliseconds) to this start time. + // E.g. + // start_timestamp = 1674819807315 (UTC 27 Jan 2023 in the morning) + // relative_timestamp = 3000 (3s) + int64 start_timestamp = 16; + GNSSPosition initial_position = 17; + repeated GNSSPosition corrected_positions = 18; + + SensorInfo accelerometer_info = 19; + SensorInfo gyroscope_info = 20; + SensorInfo rotation_vector_info = 21; + SensorInfo magnetometer_info = 22; + SensorInfo barometer_info = 23; + SensorInfo light_sensor_info = 24; + SensorInfo proximity_info = 25; + + repeated TimestampMarker test_points = 26; + + // Initial heading/bearing in degrees (0-360) + optional float initial_heading = 27; + + // Building/venue name for indoor mapping + optional string venue_name = 28; + + // Building ID for linking to indoor map APIs + optional string building_id = 29; } -message Pdr_Sample { -// milliseconds from the start_timestamp -int64 relative_timestamp = 1; +message RelativePosition { + // milliseconds from the start_timestamp + int64 relative_timestamp = 1; -// Both in metres. You should implement an algorithm to estimate -// these values. The values are always relative to your start point -// so the first entry should always be x = 0.0, y = 0.0 -float x = 2; -float y = 3; + // Both in metres. You should implement an algorithm to estimate + // these values. The values are always relative to your start point + // so the first entry should always be x = 0.0, y = 0.0 + float x = 2; + float y = 3; } +message IMUReading { + // milliseconds + int64 relative_timestamp = 1; + // Accelerometer [m/s^2] + Vector3 acc = 2; + + // Gyroscope [radians/s] + Vector3 gyr = 3; + + // Orientation [unitless], 4 components should square sum to ~1 + Quaternion rotation_vector = 4; + + // Number of steps so far + int32 step_count = 5; +} + +message MagnetometerReading { + int64 relative_timestamp = 1; -message Motion_Sample { - // milliseconds - int64 relative_timestamp = 1; - // m/s^2 - float acc_x = 2; - float acc_y = 3; - float acc_z = 4; - - // radians/s - float gyr_x = 5; - float gyr_y = 6; - float gyr_z = 7; - - // unitless, 4 components should sum to ~1 - float rotation_vector_x = 8; - float rotation_vector_y = 9; - float rotation_vector_z = 10; - float rotation_vector_w = 11; - - // Integer - int32 step_count = 12; + // Magnetometer [uT] + Vector3 mag = 2; } + +message BarometerReading { + int64 relative_timestamp = 1; -message Position_Sample { - int64 relative_timestamp = 1; + // mbar + float pressure = 2; +} - // uT - float mag_x = 2; - float mag_y = 3; - float mag_z = 4; +message LightReading { + int64 relative_timestamp = 1; + // lux + float light = 2; } -message Pressure_Sample { - int64 relative_timestamp = 1; +message ProximityReading { + int64 relative_timestamp = 1; + // cm + float distance = 2; +} - // mbar - float pressure = 2; +message GNSSPosition { + int64 relative_timestamp = 1; + + // degrees (minimum 6 significant figures) + // latitude between -90 and 90 + double latitude = 2; + // longitude between -180 and 180 + double longitude = 3; + //metres + double altitude = 4; + // floor name + optional string floor = 5; +} +// Timestamp marker for marking specific points during trajectory collection +message TimestampMarker { + int64 relative_timestamp = 1; + + // degrees (minimum 6 significant figures) + double latitude = 2; + // longitude between -180 and 180 + double longitude = 3; + // metres + double altitude = 4; + + // Marker sequence number (0-indexed) + int32 marker_index = 5; + + // Optional marker name/description + optional string marker_name = 6; + + // Floor information + optional string floor = 7; } -message Light_Sample { - int64 relative_timestamp = 1; - // lux - float light = 2; + +message GNSSReading { + GNSSPosition position = 1; + // metres + float accuracy = 2; + // m/s + float speed = 3; + // degrees + float bearing = 4; + + // e.g 'gps' or 'network' + string provider = 5; } -message GNSS_Sample { - int64 relative_timestamp = 1; - // degrees (minimum 6 significant figures) - // latitude between -90 and 90 - float latitude = 2; +message Fingerprint { + int64 relative_timestamp = 1; + repeated RFScan rf_scans = 2; - // longitude between -180 and 180 - float longitude = 3; +} - //metres - float altitude = 4; +message RFScan { + int64 relative_timestamp = 1; - // metres - float accuracy = 5; + // Integer encoding of the hex mac address (BSSID) + // e.g. 207394925843984 + int64 mac = 2; - // m/s - float speed = 6; + // rssi integer in dBm. + // typically between -120 and -10 + int32 rssi = 3; - // e.g 'gps' or 'network' - string provider = 7; + // returned position + optional GNSSPosition position = 4; } -message WiFi_Sample { - int64 relative_timestamp = 1; - repeated Mac_Scan mac_scans = 2; - +message WiFiRTTReading { + int64 relative_timestamp = 1; + // cm + // Integer encoding of the hex mac address (BSSID) + // e.g. 207394925843984 + int64 mac = 2; + + // in mm + float distance = 3; + // in mm + float distance_std = 4; + // rssi integer in dBm. + // typically between -120 and -10 + int32 rssi = 5; } -message Mac_Scan { - int64 relative_timestamp = 1; +message WiFiAPData { + // Integer encoding of the hex mac address (BSSID) + // e.g. 207394925843984 + int64 mac = 1; - // Integer encoding of the hex mac address - // e.g. 207394925843984 - int64 mac = 2; + // E.g. 'Eduroam' or 'Starbucks_free_wifi' + string ssid = 2; - // rssi integer in dBm. - // typically between -120 and -10 - int32 rssi = 3; + // Typically 2.4GHz or 5GHz + int64 frequency = 3; + + // Flag to indicate if the AP supports RTT measurements + bool rtt_enabled = 4; } -message AP_Data { - // Integer encoding of the hex mac address - // e.g. 207394925843984 - int64 mac = 1; +message BleData { + string mac_address = 1; + string name = 2; + int32 tx_power_level = 3; + int32 advertise_flags = 4; + repeated string service_uuids = 5; + bytes manufacturer_data = 6; +} + + // --- Common Types --- +message Vector3 { + float x = 1; + float y = 2; + float z = 3; +} - // E.g. 'Eduroam' or 'Starbucks_free_wifi' - string ssid = 2; +message Quaternion { + float x = 1; + float y = 2; + float z = 3; + float w = 4; +} - // Typically 2.4GHz or 5GHz - int64 frequency = 3; +message SensorInfo { + string name = 1; + string vendor = 2; + float resolution = 3; + float power = 4; + int32 version = 5; + int32 type = 6; + float max_range = 7; + float frequency = 8; } -message Sensor_Info { - string name = 1; - string vendor = 2; - float resolution = 3; - float power = 4; - int32 version = 5; - int32 type = 6; -} \ No newline at end of file diff --git a/app/src/main/res/animator/home_shortcut_press_down.xml b/app/src/main/res/animator/home_shortcut_press_down.xml new file mode 100644 index 00000000..b44c3d40 --- /dev/null +++ b/app/src/main/res/animator/home_shortcut_press_down.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/animator/home_shortcut_press_up.xml b/app/src/main/res/animator/home_shortcut_press_up.xml new file mode 100644 index 00000000..27a107cf --- /dev/null +++ b/app/src/main/res/animator/home_shortcut_press_up.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/animator/home_shortcut_state_animator.xml b/app/src/main/res/animator/home_shortcut_state_animator.xml new file mode 100644 index 00000000..c4dc1bd0 --- /dev/null +++ b/app/src/main/res/animator/home_shortcut_state_animator.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/drawable/seekbar_thumb_circle.xml b/app/src/main/res/drawable/seekbar_thumb_circle.xml new file mode 100644 index 00000000..fd0b8246 --- /dev/null +++ b/app/src/main/res/drawable/seekbar_thumb_circle.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/drawable/status_background.xml b/app/src/main/res/drawable/status_background.xml new file mode 100644 index 00000000..1e7847fb --- /dev/null +++ b/app/src/main/res/drawable/status_background.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/status_recording.xml b/app/src/main/res/drawable/status_recording.xml new file mode 100644 index 00000000..08c03e6c --- /dev/null +++ b/app/src/main/res/drawable/status_recording.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/layout-small/fragment_home.xml b/app/src/main/res/layout-small/fragment_home.xml index bd713b67..47cfce4b 100644 --- a/app/src/main/res/layout-small/fragment_home.xml +++ b/app/src/main/res/layout-small/fragment_home.xml @@ -10,8 +10,10 @@ + android:paddingHorizontal="20dp" + android:paddingTop="12dp" + android:paddingBottom="20dp" + android:background="@color/ios_background"> @@ -34,9 +37,8 @@ android:layout_height="wrap_content" android:text="@string/fragment_home_slogan" android:textSize="14sp" - android:textStyle="italic" android:textAlignment="center" - android:textColor="@color/md_theme_primary" + android:textColor="@color/ios_secondary_label" app:layout_constraintTop_toBottomOf="@id/pageTitle" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -48,8 +50,11 @@ android:id="@+id/gnssStatusTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textColor="@android:color/holo_red_dark" - android:textSize="16sp" + android:background="@drawable/status_background" + android:paddingHorizontal="14dp" + android:paddingVertical="8dp" + android:textColor="@color/ios_red" + android:textSize="13sp" android:visibility="gone" android:text="GNSS is disabled" app:layout_constraintTop_toBottomOf="@id/sloganText" @@ -58,16 +63,23 @@ android:layout_marginTop="8dp" /> - + app:layout_constraintDimensionRatio="16:10"> + + + @@ -85,88 +97,77 @@ + app:iconSize="30dp" /> + app:iconSize="30dp" /> + app:iconSize="30dp" /> - - - - + app:layout_constraintEnd_toEndOf="parent" + android:layout_marginTop="4dp" /> diff --git a/app/src/main/res/layout/fragment_correction.xml b/app/src/main/res/layout/fragment_correction.xml index ce536570..cce410c8 100644 --- a/app/src/main/res/layout/fragment_correction.xml +++ b/app/src/main/res/layout/fragment_correction.xml @@ -4,127 +4,29 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + tools:context=".presentation.fragment.CorrectionFragment"> - - - - - - - - - - - - - - - - - - - - - - - - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - - - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_files.xml b/app/src/main/res/layout/fragment_files.xml index 03de3f60..34c4b148 100644 --- a/app/src/main/res/layout/fragment_files.xml +++ b/app/src/main/res/layout/fragment_files.xml @@ -8,21 +8,17 @@ + android:layout_height="match_parent" + android:background="@color/ios_background" + android:paddingHorizontal="16dp" + android:paddingTop="12dp"> - + app:tint="@color/ios_blue" /> - + android:paddingHorizontal="20dp" + android:paddingTop="12dp" + android:paddingBottom="20dp" + android:background="@color/ios_background"> - - + android:layout_marginBottom="18dp" /> - - - + app:layout_constraintDimensionRatio="16:10"> + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_info.xml b/app/src/main/res/layout/fragment_info.xml index 43fb43b6..6ee0e48e 100644 --- a/app/src/main/res/layout/fragment_info.xml +++ b/app/src/main/res/layout/fragment_info.xml @@ -4,20 +4,28 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:background="@color/ios_background" tools:context=".presentation.fragment.InfoFragment"> + android:layout_height="match_parent" + android:paddingStart="12dp" + android:paddingTop="8dp" + android:paddingEnd="12dp" + android:paddingBottom="12dp"> - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_measurements.xml b/app/src/main/res/layout/fragment_measurements.xml index 640af10d..26770818 100644 --- a/app/src/main/res/layout/fragment_measurements.xml +++ b/app/src/main/res/layout/fragment_measurements.xml @@ -4,610 +4,310 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:background="@color/ios_background" tools:context=".presentation.fragment.MeasurementsFragment"> + android:layout_height="match_parent" + android:paddingStart="12dp" + android:paddingTop="8dp" + android:paddingEnd="12dp" + android:paddingBottom="12dp"> + app:layout_constraintTop_toTopOf="parent"> - + android:layout_height="44dp" + android:layout_marginTop="4dp" + app:cardBackgroundColor="@color/ios_surface" + app:cardCornerRadius="18dp" + app:cardElevation="0dp" + app:layout_constraintTop_toTopOf="parent" + app:strokeColor="@color/ios_separator" + app:strokeWidth="1dp"> - - - - - - - - - + android:layout_height="match_parent" + android:paddingStart="14dp" + android:paddingEnd="14dp"> + + + + + - + - + android:layout_height="44dp" + android:layout_marginTop="8dp" + app:cardBackgroundColor="@color/ios_surface" + app:cardCornerRadius="18dp" + app:cardElevation="0dp" + app:layout_constraintTop_toBottomOf="@id/accelerometerView" + app:strokeColor="@color/ios_separator" + app:strokeWidth="1dp"> - - - - - - - - - + android:layout_height="match_parent" + android:paddingStart="14dp" + android:paddingEnd="14dp"> + + + + + - + - + android:layout_height="44dp" + android:layout_marginTop="8dp" + app:cardBackgroundColor="@color/ios_surface" + app:cardCornerRadius="18dp" + app:cardElevation="0dp" + app:layout_constraintTop_toBottomOf="@id/gravityView" + app:strokeColor="@color/ios_separator" + app:strokeWidth="1dp"> - - - - - - - - - + android:layout_height="match_parent" + android:paddingStart="14dp" + android:paddingEnd="14dp"> + + + + + - + - + android:layout_height="44dp" + android:layout_marginTop="8dp" + app:cardBackgroundColor="@color/ios_surface" + app:cardCornerRadius="18dp" + app:cardElevation="0dp" + app:layout_constraintTop_toBottomOf="@id/magneticFieldView" + app:strokeColor="@color/ios_separator" + app:strokeWidth="1dp"> - - - - - - - - - + android:layout_height="match_parent" + android:paddingStart="14dp" + android:paddingEnd="14dp"> + + + + + - + - + android:layout_height="44dp" + android:layout_marginTop="8dp" + app:cardBackgroundColor="@color/ios_surface" + app:cardCornerRadius="18dp" + app:cardElevation="0dp" + app:layout_constraintTop_toBottomOf="@id/gyroscopeView" + app:strokeColor="@color/ios_separator" + app:strokeWidth="1dp"> - - - - + android:layout_height="match_parent" + android:paddingStart="14dp" + android:paddingEnd="14dp"> + + - + - + android:layout_height="44dp" + android:layout_marginTop="8dp" + app:cardBackgroundColor="@color/ios_surface" + app:cardCornerRadius="18dp" + app:cardElevation="0dp" + app:layout_constraintTop_toBottomOf="@id/lightSensorView" + app:strokeColor="@color/ios_separator" + app:strokeWidth="1dp"> - - - - + android:layout_height="match_parent" + android:paddingStart="14dp" + android:paddingEnd="14dp"> + + - + - + android:layout_height="44dp" + android:layout_marginTop="8dp" + app:cardBackgroundColor="@color/ios_surface" + app:cardCornerRadius="18dp" + app:cardElevation="0dp" + app:layout_constraintTop_toBottomOf="@id/pressureSensorView" + app:strokeColor="@color/ios_separator" + app:strokeWidth="1dp"> - - - - + android:layout_height="match_parent" + android:paddingStart="14dp" + android:paddingEnd="14dp"> + + - + - + android:layout_height="44dp" + android:layout_marginTop="8dp" + app:cardBackgroundColor="@color/ios_surface" + app:cardCornerRadius="18dp" + app:cardElevation="0dp" + app:layout_constraintTop_toBottomOf="@id/proximityView" + app:strokeColor="@color/ios_separator" + app:strokeWidth="1dp"> - - - - - - + android:layout_height="match_parent" + android:paddingStart="14dp" + android:paddingEnd="14dp"> + + + - + - + android:layout_height="44dp" + android:layout_marginTop="8dp" + app:cardBackgroundColor="@color/ios_surface" + app:cardCornerRadius="18dp" + app:cardElevation="0dp" + app:layout_constraintTop_toBottomOf="@id/gnssView" + app:strokeColor="@color/ios_separator" + app:strokeWidth="1dp"> - - - - - - + android:layout_height="match_parent" + android:paddingStart="14dp" + android:paddingEnd="14dp"> + + + - - - + - + app:layout_constraintTop_toBottomOf="@id/sensorMeasurementList" + app:strokeColor="@color/ios_separator" + app:strokeWidth="1dp"> - - - - - - + android:layout_height="match_parent" + android:paddingStart="16dp" + android:paddingEnd="16dp"> + + + + - + + + + + + + + - \ No newline at end of file + + + diff --git a/app/src/main/res/layout/fragment_recording.xml b/app/src/main/res/layout/fragment_recording.xml index c04381a5..e950df48 100644 --- a/app/src/main/res/layout/fragment_recording.xml +++ b/app/src/main/res/layout/fragment_recording.xml @@ -1,145 +1,188 @@ - + android:layout_height="match_parent" + android:background="@color/ios_background" + tools:context=".presentation.fragment.RecordingFragment"> - + + + + + + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent"> - - - - - - + android:orientation="vertical" + android:paddingHorizontal="20dp" + android:paddingTop="20dp" + android:paddingBottom="18dp"> - - + android:gravity="center_vertical" + android:orientation="horizontal"> + + + + + + + + + - - - + android:background="@drawable/status_background" + android:layout_marginTop="12dp" + android:minHeight="72dp" + android:paddingHorizontal="16dp" + android:paddingVertical="14dp" + android:text="Ready to record" + android:textColor="@color/ios_secondary_label" + android:textSize="13sp" + android:lineSpacingExtra="2dp" /> - - - - - + - - + - - - - - - - - + + + + + + + + + + + + - + + diff --git a/app/src/main/res/layout/fragment_replay.xml b/app/src/main/res/layout/fragment_replay.xml index b6e88834..55475553 100644 --- a/app/src/main/res/layout/fragment_replay.xml +++ b/app/src/main/res/layout/fragment_replay.xml @@ -4,65 +4,73 @@ android:id="@+id/replayRoot" android:layout_width="match_parent" android:layout_height="match_parent" - android:orientation="vertical"> + android:orientation="vertical" + android:background="@color/ios_background" + android:padding="12dp"> - - + android:layout_weight="1"> + + - + android:layout_marginTop="12dp" + android:thumb="@drawable/seekbar_thumb_circle" + android:thumbOffset="0dp" /> - + android:layout_marginTop="12dp"> -