After Width: | Height: | Size: 19 KiB |
@ -0,0 +1,174 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.android.apps.location.gps.gnsslogger; |
||||
|
||||
import android.app.Activity; |
||||
import android.app.Fragment; |
||||
import android.content.Intent; |
||||
import android.location.LocationManager; |
||||
import android.os.Bundle; |
||||
import android.text.Editable; |
||||
import android.text.SpannableStringBuilder; |
||||
import android.text.style.ForegroundColorSpan; |
||||
import android.util.Log; |
||||
import android.view.LayoutInflater; |
||||
import android.view.View; |
||||
import android.view.View.OnClickListener; |
||||
import android.view.ViewGroup; |
||||
import android.widget.Button; |
||||
import android.widget.ScrollView; |
||||
import android.widget.TextView; |
||||
|
||||
/** The UI fragment that hosts a logging view. */ |
||||
public class AgnssFragment extends Fragment { |
||||
|
||||
public static final String TAG = ":AgnssFragment"; |
||||
private TextView mLogView; |
||||
private ScrollView mScrollView; |
||||
private GnssContainer mGpsContainer; |
||||
private AgnssUiLogger mUiLogger; |
||||
|
||||
private final AgnssUIFragmentComponent mUiComponent = new AgnssUIFragmentComponent(); |
||||
|
||||
public void setGpsContainer(GnssContainer value) { |
||||
mGpsContainer = value; |
||||
} |
||||
|
||||
public void setUILogger(AgnssUiLogger value) { |
||||
mUiLogger = value; |
||||
} |
||||
|
||||
@Override |
||||
public View onCreateView( |
||||
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { |
||||
View newView = inflater.inflate(R.layout.fragment_agnss, container, false /* attachToRoot */); |
||||
mLogView = (TextView) newView.findViewById(R.id.log_view); |
||||
mScrollView = (ScrollView) newView.findViewById(R.id.log_scroll); |
||||
|
||||
if (mUiLogger != null) { |
||||
mUiLogger.setUiFragmentComponent(mUiComponent); |
||||
} |
||||
|
||||
Button clearAgps = (Button) newView.findViewById(R.id.clearAgps); |
||||
clearAgps.setOnClickListener( |
||||
new OnClickListener() { |
||||
@Override |
||||
public void onClick(View view) { |
||||
Log.i(GnssContainer.TAG + TAG, "Clearing AGPS"); |
||||
LocationManager locationManager = mGpsContainer.getLocationManager(); |
||||
locationManager.sendExtraCommand( |
||||
LocationManager.GPS_PROVIDER, "delete_aiding_data", null); |
||||
Log.i(GnssContainer.TAG + TAG, "Clearing AGPS command sent"); |
||||
} |
||||
}); |
||||
|
||||
Button fetchExtraData = (Button) newView.findViewById(R.id.fetchExtraData); |
||||
fetchExtraData.setOnClickListener( |
||||
new OnClickListener() { |
||||
@Override |
||||
public void onClick(View view) { |
||||
Log.i(GnssContainer.TAG + TAG, "Fetching Extra data"); |
||||
LocationManager locationManager = mGpsContainer.getLocationManager(); |
||||
Bundle bundle = new Bundle(); |
||||
locationManager.sendExtraCommand("gps", "force_xtra_injection", bundle); |
||||
Log.i(GnssContainer.TAG + TAG, "Fetching Extra data Command sent"); |
||||
} |
||||
}); |
||||
|
||||
Button fetchTimeData = (Button) newView.findViewById(R.id.fetchTimeData); |
||||
fetchTimeData.setOnClickListener( |
||||
new OnClickListener() { |
||||
@Override |
||||
public void onClick(View view) { |
||||
Log.i(GnssContainer.TAG + TAG, "Fetching Time data"); |
||||
LocationManager locationManager = mGpsContainer.getLocationManager(); |
||||
Bundle bundle = new Bundle(); |
||||
locationManager.sendExtraCommand("gps", "force_time_injection", bundle); |
||||
Log.i(GnssContainer.TAG + TAG, "Fetching Time data Command sent"); |
||||
} |
||||
}); |
||||
|
||||
Button requestSingleNlp = (Button) newView.findViewById(R.id.requestSingleNlp); |
||||
requestSingleNlp.setOnClickListener( |
||||
new OnClickListener() { |
||||
@Override |
||||
public void onClick(View view) { |
||||
Log.i(GnssContainer.TAG + TAG, "Requesting Single NLP Location"); |
||||
mGpsContainer.registerSingleNetworkLocation(); |
||||
Log.i(GnssContainer.TAG + TAG, "Single NLP Location Requested"); |
||||
} |
||||
}); |
||||
|
||||
Button requestSingleGps = (Button) newView.findViewById(R.id.requestSingleGps); |
||||
requestSingleGps.setOnClickListener( |
||||
new OnClickListener() { |
||||
@Override |
||||
public void onClick(View view) { |
||||
Log.i(GnssContainer.TAG + TAG, "Requesting Single GPS Location"); |
||||
mGpsContainer.registerSingleGpsLocation(); |
||||
Log.i(GnssContainer.TAG + TAG, "Single GPS Location Requested"); |
||||
} |
||||
}); |
||||
Button clear = (Button) newView.findViewById(R.id.clear_log); |
||||
clear.setOnClickListener( |
||||
new OnClickListener() { |
||||
@Override |
||||
public void onClick(View view) { |
||||
mLogView.setText(""); |
||||
} |
||||
}); |
||||
|
||||
return newView; |
||||
} |
||||
/** A facade for Agnss UI related operations. */ |
||||
public class AgnssUIFragmentComponent { |
||||
|
||||
private static final int MAX_LENGTH = 12000; |
||||
private static final int LOWER_THRESHOLD = (int) (MAX_LENGTH * 0.5); |
||||
|
||||
public synchronized void logTextFragment(final String tag, final String text, int color) { |
||||
final SpannableStringBuilder builder = new SpannableStringBuilder(); |
||||
builder.append(tag).append(" | ").append(text).append("\n"); |
||||
builder.setSpan( |
||||
new ForegroundColorSpan(color), |
||||
0 /* start */, |
||||
builder.length(), |
||||
SpannableStringBuilder.SPAN_INCLUSIVE_EXCLUSIVE); |
||||
|
||||
Activity activity = getActivity(); |
||||
if (activity == null) { |
||||
return; |
||||
} |
||||
activity.runOnUiThread( |
||||
new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
mLogView.append(builder); |
||||
Editable editable = mLogView.getEditableText(); |
||||
int length = editable.length(); |
||||
if (length > MAX_LENGTH) { |
||||
editable.delete(0, length - LOWER_THRESHOLD); |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
|
||||
public void startActivity(Intent intent) { |
||||
getActivity().startActivity(intent); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,111 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.android.apps.location.gps.gnsslogger; |
||||
|
||||
import android.graphics.Color; |
||||
import android.location.GnssMeasurementsEvent; |
||||
import android.location.GnssNavigationMessage; |
||||
import android.location.GnssStatus; |
||||
import android.location.Location; |
||||
import android.os.Bundle; |
||||
import android.util.Log; |
||||
import com.google.android.apps.location.gps.gnsslogger.AgnssFragment.AgnssUIFragmentComponent; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
/** |
||||
* A class representing a UI logger for the application. Its responsibility is show information in |
||||
* the UI. |
||||
*/ |
||||
public class AgnssUiLogger implements GnssListener { |
||||
|
||||
private static final int USED_COLOR = Color.rgb(0x4a, 0x5f, 0x70); |
||||
|
||||
public AgnssUiLogger() {} |
||||
|
||||
private AgnssUIFragmentComponent mUiFragmentComponent; |
||||
|
||||
public synchronized AgnssUIFragmentComponent getUiFragmentComponent() { |
||||
return mUiFragmentComponent; |
||||
} |
||||
|
||||
public synchronized void setUiFragmentComponent(AgnssUIFragmentComponent value) { |
||||
mUiFragmentComponent = value; |
||||
} |
||||
|
||||
@Override |
||||
public void onProviderEnabled(String provider) { |
||||
logLocationEvent("onProviderEnabled: " + provider); |
||||
} |
||||
|
||||
@Override |
||||
public void onTTFFReceived(long l) { |
||||
logLocationEvent("timeToFirstFix: " + TimeUnit.NANOSECONDS.toMillis(l) + "millis"); |
||||
} |
||||
|
||||
@Override |
||||
public void onProviderDisabled(String provider) { |
||||
logLocationEvent("onProviderDisabled: " + provider); |
||||
} |
||||
|
||||
@Override |
||||
public void onLocationChanged(Location location) { |
||||
logLocationEvent("onLocationChanged: " + location); |
||||
} |
||||
|
||||
@Override |
||||
public void onLocationStatusChanged(String provider, int status, Bundle extras) {} |
||||
|
||||
@Override |
||||
public void onGnssMeasurementsReceived(GnssMeasurementsEvent event) {} |
||||
|
||||
@Override |
||||
public void onGnssMeasurementsStatusChanged(int status) {} |
||||
|
||||
@Override |
||||
public void onGnssNavigationMessageReceived(GnssNavigationMessage event) {} |
||||
|
||||
@Override |
||||
public void onGnssNavigationMessageStatusChanged(int status) {} |
||||
|
||||
@Override |
||||
public void onGnssStatusChanged(GnssStatus gnssStatus) {} |
||||
|
||||
@Override |
||||
public void onNmeaReceived(long timestamp, String s) {} |
||||
|
||||
@Override |
||||
public void onListenerRegistration(String listener, boolean result) { |
||||
logEvent("Registration", String.format("add%sListener: %b", listener, result), USED_COLOR); |
||||
} |
||||
|
||||
private void logEvent(String tag, String message, int color) { |
||||
String composedTag = GnssContainer.TAG + tag; |
||||
Log.d(composedTag, message); |
||||
logText(tag, message, color); |
||||
} |
||||
|
||||
private void logText(String tag, String text, int color) { |
||||
AgnssUIFragmentComponent component = getUiFragmentComponent(); |
||||
if (component != null) { |
||||
component.logTextFragment(tag, text, color); |
||||
} |
||||
} |
||||
|
||||
private void logLocationEvent(String event) { |
||||
logEvent("Location", event, USED_COLOR); |
||||
} |
||||
} |
@ -0,0 +1,48 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.android.apps.location.gps.gnsslogger; |
||||
|
||||
import android.content.BroadcastReceiver; |
||||
import android.content.Context; |
||||
import android.content.Intent; |
||||
import android.support.v4.content.LocalBroadcastManager; |
||||
|
||||
/** |
||||
* A {@link BroadcastReceiver} that receives and broadcasts the result of |
||||
* {@link com.google.android.gms.location.ActivityRecognition |
||||
* #ActivityRecognitionApi#requestActivityUpdates()} |
||||
* to {@link MainActivity} to be further analyzed. |
||||
*/ |
||||
public class DetectedActivitiesIntentReceiver extends BroadcastReceiver { |
||||
public static String AR_RESULT_BROADCAST_ACTION = |
||||
"com.google.android.apps.location.gps.gnsslogger.AR_RESULT_BROADCAST_ACTION"; |
||||
|
||||
/** |
||||
* Gets called when the result of {@link com.google.android.gms.location.ActivityRecognition |
||||
* #ActivityRecognitionApi#requestActivityUpdates()} is available and handles |
||||
* incoming intents. |
||||
* |
||||
* @param intent The Intent is provided (inside a {@link android.app.PendingIntent}) |
||||
* when {@link com.google.android.gms.location.ActivityRecognition |
||||
* #ActivityRecognitionApi#requestActivityUpdates()} is called. |
||||
*/ |
||||
public void onReceive(Context context, Intent intent) { |
||||
|
||||
intent.setAction(AR_RESULT_BROADCAST_ACTION); |
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(intent); |
||||
} |
||||
} |
@ -0,0 +1,27 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.android.apps.location.gps.gnsslogger; |
||||
|
||||
/** |
||||
* A class representing the interface toggling auto ground truth mode switch |
||||
*/ |
||||
public interface GroundTruthModeSwitcher { |
||||
/** |
||||
* Gets called to enable auto switch ground truth mode |
||||
*/ |
||||
void setAutoSwitchGroundTruthModeEnabled(boolean enabled); |
||||
} |
@ -0,0 +1,190 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.android.apps.location.gps.gnsslogger; |
||||
|
||||
import android.app.Activity; |
||||
import android.app.Fragment; |
||||
import android.os.Bundle; |
||||
import android.util.Log; |
||||
import android.view.LayoutInflater; |
||||
import android.view.View; |
||||
import android.view.ViewGroup; |
||||
import com.google.android.gms.maps.CameraUpdateFactory; |
||||
import com.google.android.gms.maps.GoogleMap; |
||||
import com.google.android.gms.maps.MapView; |
||||
import com.google.android.gms.maps.MapsInitializer; |
||||
import com.google.android.gms.maps.OnMapReadyCallback; |
||||
import com.google.android.gms.maps.model.BitmapDescriptorFactory; |
||||
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 java.text.SimpleDateFormat; |
||||
import java.util.Date; |
||||
import java.util.HashSet; |
||||
import java.util.Set; |
||||
|
||||
/** |
||||
* A map fragment to show the computed least square position and the device computed position on |
||||
* Google map. |
||||
*/ |
||||
public class MapFragment extends Fragment implements OnMapReadyCallback { |
||||
private static final float ZOOM_LEVEL = 15; |
||||
private static final String TAG = "MapFragment"; |
||||
private RealTimePositionVelocityCalculator mPositionVelocityCalculator; |
||||
|
||||
private static final SimpleDateFormat DATE_SDF = new SimpleDateFormat("HH:mm:ss"); |
||||
|
||||
// UI members
|
||||
private GoogleMap mMap; // Might be null if Google Play services APK is not available.
|
||||
private MapView mMapView; |
||||
private final Set<Object> mSetOfFeatures = new HashSet<Object>(); |
||||
|
||||
private Marker mLastLocationMarkerRaw = null; |
||||
private Marker mLastLocationMarkerDevice = null; |
||||
|
||||
@Override |
||||
public View onCreateView( |
||||
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { |
||||
View rootView = inflater.inflate(R.layout.map_fragment, container, false); |
||||
mMapView = ((MapView) rootView.findViewById(R.id.map)); |
||||
mMapView.onCreate(savedInstanceState); |
||||
mMapView.getMapAsync(this); |
||||
MapsInitializer.initialize(getActivity()); |
||||
|
||||
RealTimePositionVelocityCalculator currentPositionVelocityCalculator = |
||||
mPositionVelocityCalculator; |
||||
if (currentPositionVelocityCalculator != null) { |
||||
currentPositionVelocityCalculator.setMapFragment(this); |
||||
} |
||||
|
||||
return rootView; |
||||
} |
||||
|
||||
@Override |
||||
public void onResume() { |
||||
super.onResume(); |
||||
mMapView.onResume(); |
||||
if (mMap != null) { |
||||
mMap.clear(); |
||||
} |
||||
mLastLocationMarkerRaw = null; |
||||
mLastLocationMarkerDevice = null; |
||||
} |
||||
|
||||
@Override |
||||
public void onPause() { |
||||
mMapView.onPause(); |
||||
super.onPause(); |
||||
} |
||||
|
||||
@Override |
||||
public void onDestroy() { |
||||
mMapView.onDestroy(); |
||||
super.onDestroy(); |
||||
} |
||||
|
||||
@Override |
||||
public void onMapReady(GoogleMap googleMap) { |
||||
mMap = googleMap; |
||||
mMap.setMyLocationEnabled(false); |
||||
mMap.getUiSettings().setZoomControlsEnabled(true); |
||||
mMap.getUiSettings().setZoomGesturesEnabled(true); |
||||
mMap.getUiSettings().setMapToolbarEnabled(false); |
||||
} |
||||
|
||||
public void setPositionVelocityCalculator(RealTimePositionVelocityCalculator value) { |
||||
mPositionVelocityCalculator = value; |
||||
} |
||||
|
||||
public void updateMapViewWithPostions( |
||||
final double latDegRaw, |
||||
final double lngDegRaw, |
||||
final double latDegDevice, |
||||
final double lngDegDevice, |
||||
final long timeMillis) { |
||||
Activity activity = getActivity(); |
||||
if (activity == null) { |
||||
return; |
||||
} |
||||
activity.runOnUiThread( |
||||
new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
Log.i(TAG, "onLocationChanged"); |
||||
LatLng latLngRaw = new LatLng(latDegRaw, lngDegRaw); |
||||
LatLng latLngDevice = new LatLng(latDegDevice, lngDegDevice); |
||||
if (mLastLocationMarkerRaw == null && mLastLocationMarkerDevice == null) { |
||||
if (mMap != null) { |
||||
mLastLocationMarkerDevice = |
||||
mMap.addMarker( |
||||
new MarkerOptions() |
||||
.position(latLngDevice) |
||||
.title(getResources().getString(R.string.title_device)) |
||||
.icon( |
||||
BitmapDescriptorFactory.defaultMarker( |
||||
BitmapDescriptorFactory.HUE_BLUE))); |
||||
mLastLocationMarkerDevice.showInfoWindow(); |
||||
|
||||
mLastLocationMarkerRaw = |
||||
mMap.addMarker( |
||||
new MarkerOptions() |
||||
.position(latLngRaw) |
||||
.title(getResources().getString(R.string.title_wls)) |
||||
.icon( |
||||
BitmapDescriptorFactory.defaultMarker( |
||||
BitmapDescriptorFactory.HUE_GREEN))); |
||||
mLastLocationMarkerRaw.showInfoWindow(); |
||||
|
||||
mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(latLngRaw, ZOOM_LEVEL)); |
||||
} |
||||
} else { |
||||
mLastLocationMarkerRaw.setPosition(latLngRaw); |
||||
mLastLocationMarkerDevice.setPosition(latLngDevice); |
||||
} |
||||
if (mLastLocationMarkerRaw == null && mLastLocationMarkerDevice == null) { |
||||
String formattedDate = DATE_SDF.format(new Date(timeMillis)); |
||||
mLastLocationMarkerRaw.setTitle("time: " + formattedDate); |
||||
mLastLocationMarkerDevice.showInfoWindow(); |
||||
|
||||
mLastLocationMarkerRaw.setTitle("time: " + formattedDate); |
||||
mLastLocationMarkerDevice.showInfoWindow(); |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
|
||||
public void clearMarkers() { |
||||
Activity activity = getActivity(); |
||||
if (activity == null) { |
||||
return; |
||||
} |
||||
activity.runOnUiThread( |
||||
new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
if (mLastLocationMarkerRaw != null) { |
||||
mLastLocationMarkerRaw.remove(); |
||||
mLastLocationMarkerRaw = null; |
||||
} |
||||
if (mLastLocationMarkerDevice != null) { |
||||
mLastLocationMarkerDevice.remove(); |
||||
mLastLocationMarkerDevice = null; |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,464 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.android.apps.location.gps.gnsslogger; |
||||
|
||||
import android.app.Fragment; |
||||
import android.content.Context; |
||||
import android.graphics.Color; |
||||
import android.graphics.Paint.Align; |
||||
import android.location.GnssMeasurement; |
||||
import android.location.GnssMeasurementsEvent; |
||||
import android.location.GnssStatus; |
||||
import android.os.Bundle; |
||||
import android.support.v4.util.ArrayMap; |
||||
import android.text.Spannable; |
||||
import android.text.SpannableStringBuilder; |
||||
import android.text.style.ForegroundColorSpan; |
||||
import android.view.LayoutInflater; |
||||
import android.view.View; |
||||
import android.view.ViewGroup; |
||||
import android.widget.AdapterView; |
||||
import android.widget.AdapterView.OnItemSelectedListener; |
||||
import android.widget.LinearLayout; |
||||
import android.widget.Spinner; |
||||
import android.widget.TextView; |
||||
import com.google.location.lbs.gnss.gps.pseudorange.GpsNavigationMessageStore; |
||||
import java.text.DecimalFormat; |
||||
import java.util.ArrayList; |
||||
import java.util.Collections; |
||||
import java.util.Comparator; |
||||
import java.util.List; |
||||
import java.util.Random; |
||||
import java.util.concurrent.TimeUnit; |
||||
import org.achartengine.ChartFactory; |
||||
import org.achartengine.GraphicalView; |
||||
import org.achartengine.model.XYMultipleSeriesDataset; |
||||
import org.achartengine.model.XYSeries; |
||||
import org.achartengine.renderer.XYMultipleSeriesRenderer; |
||||
import org.achartengine.renderer.XYSeriesRenderer; |
||||
import org.achartengine.util.MathHelper; |
||||
|
||||
/** A plot fragment to show real-time Gnss analysis migrated from GnssAnalysis Tool. */ |
||||
public class PlotFragment extends Fragment { |
||||
|
||||
/** Total number of kinds of plot tabs */ |
||||
private static final int NUMBER_OF_TABS = 2; |
||||
|
||||
/** The position of the CN0 over time plot tab */ |
||||
private static final int CN0_TAB = 0; |
||||
|
||||
/** The position of the prearrange residual plot tab*/ |
||||
private static final int PR_RESIDUAL_TAB = 1; |
||||
|
||||
/** The number of Gnss constellations */ |
||||
private static final int NUMBER_OF_CONSTELLATIONS = 6; |
||||
|
||||
/** The X range of the plot, we are keeping the latest one minute visible */ |
||||
private static final double TIME_INTERVAL_SECONDS = 60; |
||||
|
||||
/** The index in data set we reserved for the plot containing all constellations */ |
||||
private static final int DATA_SET_INDEX_ALL = 0; |
||||
|
||||
/** The number of satellites we pick for the strongest satellite signal strength calculation */ |
||||
private static final int NUMBER_OF_STRONGEST_SATELLITES = 4; |
||||
|
||||
/** Data format used to format the data in the text view */ |
||||
private static final DecimalFormat sDataFormat = new DecimalFormat("##.#"); |
||||
|
||||
private GraphicalView mChartView; |
||||
|
||||
/** The average of the average of strongest satellite signal strength over history */ |
||||
private double mAverageCn0 = 0; |
||||
|
||||
/** Total number of {@link GnssMeasurementsEvent} has been recieved*/ |
||||
private int mMeasurementCount = 0; |
||||
private double mInitialTimeSeconds = -1; |
||||
private TextView mAnalysisView; |
||||
private double mLastTimeReceivedSeconds = 0; |
||||
private final ColorMap mColorMap = new ColorMap(); |
||||
private DataSetManager mDataSetManager; |
||||
private XYMultipleSeriesRenderer mCurrentRenderer; |
||||
private LinearLayout mLayout; |
||||
private int mCurrentTab = 0; |
||||
|
||||
@Override |
||||
public View onCreateView( |
||||
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { |
||||
View plotView = inflater.inflate(R.layout.fragment_plot, container, false /* attachToRoot */); |
||||
|
||||
mDataSetManager |
||||
= new DataSetManager(NUMBER_OF_TABS, NUMBER_OF_CONSTELLATIONS, getContext(), mColorMap); |
||||
|
||||
// Set UI elements handlers
|
||||
final Spinner spinner = plotView.findViewById(R.id.constellation_spinner); |
||||
final Spinner tabSpinner = plotView.findViewById(R.id.tab_spinner); |
||||
|
||||
OnItemSelectedListener spinnerOnSelectedListener = new OnItemSelectedListener() { |
||||
|
||||
@Override |
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { |
||||
mCurrentTab = tabSpinner.getSelectedItemPosition(); |
||||
XYMultipleSeriesRenderer renderer |
||||
= mDataSetManager.getRenderer(mCurrentTab, spinner.getSelectedItemPosition()); |
||||
XYMultipleSeriesDataset dataSet |
||||
= mDataSetManager.getDataSet(mCurrentTab, spinner.getSelectedItemPosition()); |
||||
if (mLastTimeReceivedSeconds > TIME_INTERVAL_SECONDS) { |
||||
renderer.setXAxisMax(mLastTimeReceivedSeconds); |
||||
renderer.setXAxisMin(mLastTimeReceivedSeconds - TIME_INTERVAL_SECONDS); |
||||
} |
||||
mCurrentRenderer = renderer; |
||||
mLayout.removeAllViews(); |
||||
mChartView = ChartFactory.getLineChartView(getContext(), dataSet, renderer); |
||||
mLayout.addView(mChartView); |
||||
} |
||||
|
||||
@Override |
||||
public void onNothingSelected(AdapterView<?> parent) {} |
||||
}; |
||||
|
||||
spinner.setOnItemSelectedListener(spinnerOnSelectedListener); |
||||
tabSpinner.setOnItemSelectedListener(spinnerOnSelectedListener); |
||||
|
||||
// Set up the Graph View
|
||||
mCurrentRenderer = mDataSetManager.getRenderer(mCurrentTab, DATA_SET_INDEX_ALL); |
||||
XYMultipleSeriesDataset currentDataSet |
||||
= mDataSetManager.getDataSet(mCurrentTab, DATA_SET_INDEX_ALL); |
||||
mChartView = ChartFactory.getLineChartView(getContext(), currentDataSet, mCurrentRenderer); |
||||
mAnalysisView = plotView.findViewById(R.id.analysis); |
||||
mAnalysisView.setTextColor(Color.BLACK); |
||||
mLayout = plotView.findViewById(R.id.plot); |
||||
mLayout.addView(mChartView); |
||||
return plotView; |
||||
} |
||||
|
||||
/** |
||||
* Updates the CN0 versus Time plot data from a {@link GnssMeasurement} |
||||
*/ |
||||
protected void updateCnoTab(GnssMeasurementsEvent event) { |
||||
long timeInSeconds = |
||||
TimeUnit.NANOSECONDS.toSeconds(event.getClock().getTimeNanos()); |
||||
if (mInitialTimeSeconds < 0) { |
||||
mInitialTimeSeconds = timeInSeconds; |
||||
} |
||||
|
||||
// Building the texts message in analysis text view
|
||||
List<GnssMeasurement> measurements = |
||||
sortByCarrierToNoiseRatio(new ArrayList<>(event.getMeasurements())); |
||||
SpannableStringBuilder builder = new SpannableStringBuilder(); |
||||
double currentAverage = 0; |
||||
if (measurements.size() >= NUMBER_OF_STRONGEST_SATELLITES) { |
||||
mAverageCn0 = |
||||
(mAverageCn0 * mMeasurementCount |
||||
+ (measurements.get(0).getCn0DbHz() |
||||
+ measurements.get(1).getCn0DbHz() |
||||
+ measurements.get(2).getCn0DbHz() |
||||
+ measurements.get(3).getCn0DbHz()) |
||||
/ NUMBER_OF_STRONGEST_SATELLITES) |
||||
/ (++mMeasurementCount); |
||||
currentAverage = |
||||
(measurements.get(0).getCn0DbHz() |
||||
+ measurements.get(1).getCn0DbHz() |
||||
+ measurements.get(2).getCn0DbHz() |
||||
+ measurements.get(3).getCn0DbHz()) |
||||
/ NUMBER_OF_STRONGEST_SATELLITES; |
||||
} |
||||
builder.append(getString(R.string.history_average_hint, |
||||
sDataFormat.format(mAverageCn0) + "\n")); |
||||
builder.append(getString(R.string.current_average_hint, |
||||
sDataFormat.format(currentAverage) + "\n")); |
||||
for (int i = 0; i < NUMBER_OF_STRONGEST_SATELLITES && i < measurements.size(); i++) { |
||||
int start = builder.length(); |
||||
builder.append( |
||||
mDataSetManager.getConstellationPrefix(measurements.get(i).getConstellationType()) |
||||
+ measurements.get(i).getSvid() |
||||
+ ": " |
||||
+ sDataFormat.format(measurements.get(i).getCn0DbHz()) |
||||
+ "\n"); |
||||
int end = builder.length(); |
||||
builder.setSpan( |
||||
new ForegroundColorSpan( |
||||
mColorMap.getColor( |
||||
measurements.get(i).getSvid(), measurements.get(i).getConstellationType())), |
||||
start, |
||||
end, |
||||
Spannable.SPAN_INCLUSIVE_EXCLUSIVE); |
||||
} |
||||
builder.append(getString(R.string.satellite_number_sum_hint, measurements.size())); |
||||
mAnalysisView.setText(builder); |
||||
|
||||
// Adding incoming data into Dataset
|
||||
mLastTimeReceivedSeconds = timeInSeconds - mInitialTimeSeconds; |
||||
for (GnssMeasurement measurement : measurements) { |
||||
int constellationType = measurement.getConstellationType(); |
||||
int svID = measurement.getSvid(); |
||||
if (constellationType != GnssStatus.CONSTELLATION_UNKNOWN) { |
||||
mDataSetManager.addValue( |
||||
CN0_TAB, |
||||
constellationType, |
||||
svID, |
||||
mLastTimeReceivedSeconds, |
||||
measurement.getCn0DbHz()); |
||||
} |
||||
} |
||||
|
||||
mDataSetManager.fillInDiscontinuity(CN0_TAB, mLastTimeReceivedSeconds); |
||||
|
||||
// Checks if the plot has reached the end of frame and resize
|
||||
if (mLastTimeReceivedSeconds > mCurrentRenderer.getXAxisMax()) { |
||||
mCurrentRenderer.setXAxisMax(mLastTimeReceivedSeconds); |
||||
mCurrentRenderer.setXAxisMin(mLastTimeReceivedSeconds - TIME_INTERVAL_SECONDS); |
||||
} |
||||
|
||||
mChartView.invalidate(); |
||||
} |
||||
|
||||
/** |
||||
* Updates the pseudorange residual plot from residual results calculated by |
||||
* {@link RealTimePositionVelocityCalculator} |
||||
* |
||||
* @param residuals An array of MAX_NUMBER_OF_SATELLITES elements where indexes of satellites was |
||||
* not seen are fixed with {@code Double.NaN} and indexes of satellites what were seen |
||||
* are filled with pseudorange residual in meters |
||||
* @param timeInSeconds the time at which measurements are received |
||||
*/ |
||||
protected void updatePseudorangeResidualTab(double[] residuals, double timeInSeconds) { |
||||
double timeSinceLastMeasurement = timeInSeconds - mInitialTimeSeconds; |
||||
for (int i = 1; i <= GpsNavigationMessageStore.MAX_NUMBER_OF_SATELLITES; i++) { |
||||
if (!Double.isNaN(residuals[i - 1])) { |
||||
mDataSetManager.addValue( |
||||
PR_RESIDUAL_TAB, |
||||
GnssStatus.CONSTELLATION_GPS, |
||||
i, |
||||
timeSinceLastMeasurement, |
||||
residuals[i - 1]); |
||||
} |
||||
} |
||||
mDataSetManager.fillInDiscontinuity(PR_RESIDUAL_TAB, timeSinceLastMeasurement); |
||||
} |
||||
|
||||
private List<GnssMeasurement> sortByCarrierToNoiseRatio(List<GnssMeasurement> measurements) { |
||||
Collections.sort( |
||||
measurements, |
||||
new Comparator<GnssMeasurement>() { |
||||
@Override |
||||
public int compare(GnssMeasurement o1, GnssMeasurement o2) { |
||||
return Double.compare(o2.getCn0DbHz(), o1.getCn0DbHz()); |
||||
} |
||||
}); |
||||
return measurements; |
||||
} |
||||
|
||||
/** |
||||
* An utility class provides and keeps record of all color assignments to the satellite in the |
||||
* plots. Each satellite will receive a unique color assignment through out every graph. |
||||
*/ |
||||
private static class ColorMap { |
||||
|
||||
private ArrayMap<Integer, Integer> mColorMap = new ArrayMap<>(); |
||||
private int mColorsAssigned = 0; |
||||
/** |
||||
* Source of Kelly's contrasting colors: |
||||
* https://medium.com/@rjurney/kellys-22-colours-of-maximum-contrast-58edb70c90d1
|
||||
*/ |
||||
private static final String[] CONTRASTING_COLORS = { |
||||
"#222222", "#F3C300", "#875692", "#F38400", "#A1CAF1", "#BE0032", "#C2B280", "#848482", |
||||
"#008856", "#E68FAC", "#0067A5", "#F99379", "#604E97", "#F6A600", "#B3446C", "#DCD300", |
||||
"#882D17", "#8DB600", "#654522", "#E25822", "#2B3D26" |
||||
}; |
||||
private final Random mRandom = new Random(); |
||||
|
||||
private int getColor(int svId, int constellationType) { |
||||
// Assign the color from Kelly's 21 contrasting colors to satellites first, if all color
|
||||
// has been assigned, use a random color and record in {@link mColorMap}.
|
||||
if (mColorMap.containsKey(constellationType * 1000 + svId)) { |
||||
return mColorMap.get(getUniqueSatelliteIdentifier(constellationType, svId)); |
||||
} |
||||
if (this.mColorsAssigned < CONTRASTING_COLORS.length) { |
||||
int color = Color.parseColor(CONTRASTING_COLORS[mColorsAssigned++]); |
||||
mColorMap.put(getUniqueSatelliteIdentifier(constellationType, svId), color); |
||||
return color; |
||||
} |
||||
int color = Color.argb(255, mRandom.nextInt(256), mRandom.nextInt(256), mRandom.nextInt(256)); |
||||
mColorMap.put(getUniqueSatelliteIdentifier(constellationType, svId), color); |
||||
return color; |
||||
} |
||||
} |
||||
|
||||
private static int getUniqueSatelliteIdentifier(int constellationType, int svID){ |
||||
return constellationType * 1000 + svID; |
||||
} |
||||
|
||||
/** |
||||
* An utility class stores and maintains all the data sets and corresponding renders. |
||||
* We use 0 as the {@code dataSetIndex} of all constellations and 1 - 6 as the |
||||
* {@code dataSetIndex} of each satellite constellations |
||||
*/ |
||||
private static class DataSetManager { |
||||
/** The Y min and max of each plot */ |
||||
private static final int[][] RENDER_HEIGHTS = {{5, 45}, {-60, 60}}; |
||||
/** |
||||
* <ul> |
||||
* <li>A list of constellation prefix</li> |
||||
* <li>G : GPS, US Constellation</li> |
||||
* <li>S : Satellite-based Augmentation System</li> |
||||
* <li>R : GLONASS, Russia Constellation</li> |
||||
* <li>J : QZSS, Japan Constellation</li> |
||||
* <li>C : BEIDOU China Constellation</li> |
||||
* <li>E : GALILEO EU Constellation</li> |
||||
* </ul> |
||||
*/ |
||||
private static final String[] CONSTELLATION_PREFIX = {"G", "S", "R", "J", "C", "E"}; |
||||
|
||||
private final List<ArrayMap<Integer, Integer>>[] mSatelliteIndex; |
||||
private final List<ArrayMap<Integer, Integer>>[] mSatelliteConstellationIndex; |
||||
private final List<XYMultipleSeriesDataset>[] mDataSetList; |
||||
private final List<XYMultipleSeriesRenderer>[] mRendererList; |
||||
private final Context mContext; |
||||
private final ColorMap mColorMap; |
||||
|
||||
public DataSetManager(int numberOfTabs, int numberOfConstellations, |
||||
Context context, ColorMap colorMap) { |
||||
mDataSetList = new ArrayList[numberOfTabs]; |
||||
mRendererList = new ArrayList[numberOfTabs]; |
||||
mSatelliteIndex = new ArrayList[numberOfTabs]; |
||||
mSatelliteConstellationIndex = new ArrayList[numberOfTabs]; |
||||
mContext = context; |
||||
mColorMap = colorMap; |
||||
|
||||
// Preparing data sets and renderer for all six constellations
|
||||
for (int i = 0; i < numberOfTabs; i++) { |
||||
mDataSetList[i] = new ArrayList<>(); |
||||
mRendererList[i] = new ArrayList<>(); |
||||
mSatelliteIndex[i] = new ArrayList<>(); |
||||
mSatelliteConstellationIndex[i] = new ArrayList<>(); |
||||
for (int k = 0; k <= numberOfConstellations; k++) { |
||||
mSatelliteIndex[i].add(new ArrayMap<Integer, Integer>()); |
||||
mSatelliteConstellationIndex[i].add(new ArrayMap<Integer, Integer>()); |
||||
XYMultipleSeriesRenderer tempRenderer = new XYMultipleSeriesRenderer(); |
||||
setUpRenderer(tempRenderer, i); |
||||
mRendererList[i].add(tempRenderer); |
||||
XYMultipleSeriesDataset tempDataSet = new XYMultipleSeriesDataset(); |
||||
mDataSetList[i].add(tempDataSet); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// The constellation type should range from 1 to 6
|
||||
private String getConstellationPrefix(int constellationType) { |
||||
if (constellationType <= GnssStatus.CONSTELLATION_UNKNOWN |
||||
|| constellationType > NUMBER_OF_CONSTELLATIONS) { |
||||
return ""; |
||||
} |
||||
return CONSTELLATION_PREFIX[constellationType - 1]; |
||||
} |
||||
|
||||
/** Returns the multiple series data set at specific tab and index */ |
||||
private XYMultipleSeriesDataset getDataSet(int tab, int dataSetIndex) { |
||||
return mDataSetList[tab].get(dataSetIndex); |
||||
} |
||||
|
||||
/** Returns the multiple series renderer set at specific tab and index */ |
||||
private XYMultipleSeriesRenderer getRenderer(int tab, int dataSetIndex) { |
||||
return mRendererList[tab].get(dataSetIndex); |
||||
} |
||||
|
||||
/** |
||||
* Adds a value into the both the data set containing all constellations and individual data set |
||||
* of the constellation of the satellite |
||||
*/ |
||||
private void addValue(int tab, int constellationType, int svID, |
||||
double timeInSeconds, double value) { |
||||
XYMultipleSeriesDataset dataSetAll = getDataSet(tab, DATA_SET_INDEX_ALL); |
||||
XYMultipleSeriesRenderer rendererAll = getRenderer(tab, DATA_SET_INDEX_ALL); |
||||
value = Double.parseDouble(sDataFormat.format(value)); |
||||
if (hasSeen(constellationType, svID, tab)) { |
||||
// If the satellite has been seen before, we retrieve the dataseries it is add and add new
|
||||
// data
|
||||
dataSetAll |
||||
.getSeriesAt(mSatelliteIndex[tab].get(constellationType).get(svID)) |
||||
.add(timeInSeconds, value); |
||||
mDataSetList[tab] |
||||
.get(constellationType) |
||||
.getSeriesAt(mSatelliteConstellationIndex[tab].get(constellationType).get(svID)) |
||||
.add(timeInSeconds, value); |
||||
} else { |
||||
// If the satellite has not been seen before, we create new dataset and renderer before
|
||||
// adding data
|
||||
mSatelliteIndex[tab].get(constellationType).put(svID, dataSetAll.getSeriesCount()); |
||||
mSatelliteConstellationIndex[tab] |
||||
.get(constellationType) |
||||
.put(svID, mDataSetList[tab].get(constellationType).getSeriesCount()); |
||||
XYSeries tempSeries = new XYSeries(CONSTELLATION_PREFIX[constellationType - 1] + svID); |
||||
tempSeries.add(timeInSeconds, value); |
||||
dataSetAll.addSeries(tempSeries); |
||||
mDataSetList[tab].get(constellationType).addSeries(tempSeries); |
||||
XYSeriesRenderer tempRenderer = new XYSeriesRenderer(); |
||||
tempRenderer.setLineWidth(5); |
||||
tempRenderer.setColor(mColorMap.getColor(svID, constellationType)); |
||||
rendererAll.addSeriesRenderer(tempRenderer); |
||||
mRendererList[tab].get(constellationType).addSeriesRenderer(tempRenderer); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Creates a discontinuity of the satellites that has been seen but not reported in this batch |
||||
* of measurements |
||||
*/ |
||||
private void fillInDiscontinuity(int tab, double referenceTimeSeconds) { |
||||
for (XYMultipleSeriesDataset dataSet : mDataSetList[tab]) { |
||||
for (int i = 0; i < dataSet.getSeriesCount(); i++) { |
||||
if (dataSet.getSeriesAt(i).getMaxX() < referenceTimeSeconds) { |
||||
dataSet.getSeriesAt(i).add(referenceTimeSeconds, MathHelper.NULL_VALUE); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Returns a boolean indicating whether the input satellite has been seen. |
||||
*/ |
||||
private boolean hasSeen(int constellationType, int svID, int tab) { |
||||
return mSatelliteIndex[tab].get(constellationType).containsKey(svID); |
||||
} |
||||
|
||||
/** |
||||
* Set up a {@link XYMultipleSeriesRenderer} with the specs customized per plot tab. |
||||
*/ |
||||
private void setUpRenderer(XYMultipleSeriesRenderer renderer, int tabNumber) { |
||||
renderer.setXAxisMin(0); |
||||
renderer.setXAxisMax(60); |
||||
renderer.setYAxisMin(RENDER_HEIGHTS[tabNumber][0]); |
||||
renderer.setYAxisMax(RENDER_HEIGHTS[tabNumber][1]); |
||||
renderer.setYAxisAlign(Align.RIGHT, 0); |
||||
renderer.setLegendTextSize(30); |
||||
renderer.setLabelsTextSize(30); |
||||
renderer.setYLabelsColor(0, Color.BLACK); |
||||
renderer.setXLabelsColor(Color.BLACK); |
||||
renderer.setFitLegend(true); |
||||
renderer.setShowGridX(true); |
||||
renderer.setMargins(new int[] {10, 10, 30, 10}); |
||||
// setting the plot untouchable
|
||||
renderer.setZoomEnabled(false, false); |
||||
renderer.setPanEnabled(false, true); |
||||
renderer.setClickEnabled(false); |
||||
renderer.setMarginsColor(Color.WHITE); |
||||
renderer.setChartTitle(mContext.getResources() |
||||
.getStringArray(R.array.plot_titles)[tabNumber]); |
||||
renderer.setChartTitleTextSize(50); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,544 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.android.apps.location.gps.gnsslogger; |
||||
|
||||
import android.graphics.Color; |
||||
import android.location.GnssMeasurementsEvent; |
||||
import android.location.GnssNavigationMessage; |
||||
import android.location.GnssStatus; |
||||
import android.location.Location; |
||||
import android.location.LocationManager; |
||||
import android.os.Bundle; |
||||
import android.os.Handler; |
||||
import android.os.HandlerThread; |
||||
import android.util.Log; |
||||
import com.google.android.apps.location.gps.gnsslogger.ResultFragment.UIResultComponent; |
||||
import com.google.location.lbs.gnss.gps.pseudorange.GpsMathOperations; |
||||
import com.google.location.lbs.gnss.gps.pseudorange.GpsNavigationMessageStore; |
||||
import com.google.location.lbs.gnss.gps.pseudorange.PseudorangePositionVelocityFromRealTimeEvents; |
||||
import java.text.DecimalFormat; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
/** |
||||
* A class that handles real time position and velocity calculation, passing {@link |
||||
* GnssMeasurementsEvent} instances to the {@link PseudorangePositionVelocityFromRealTimeEvents} |
||||
* whenever a new raw measurement is received in order to compute a new position solution. The |
||||
* computed position and velocity solutions are passed to the {@link ResultFragment} to be |
||||
* visualized. |
||||
*/ |
||||
public class RealTimePositionVelocityCalculator implements GnssListener { |
||||
/** Residual analysis where user disabled residual plots */ |
||||
public static final int RESIDUAL_MODE_DISABLED = -1; |
||||
|
||||
/** Residual analysis where the user is not moving */ |
||||
public static final int RESIDUAL_MODE_STILL = 0; |
||||
|
||||
/** Residual analysis where the user is moving fast (like driving). */ |
||||
public static final int RESIDUAL_MODE_DRIVING = 1; |
||||
|
||||
/** |
||||
* Residual analysis where the user chose to enter a LLA input as their position |
||||
*/ |
||||
public static final int RESIDUAL_MODE_AT_INPUT_LOCATION = 2; |
||||
|
||||
private static final long EARTH_RADIUS_METERS = 6371000; |
||||
private PseudorangePositionVelocityFromRealTimeEvents |
||||
mPseudorangePositionVelocityFromRealTimeEvents; |
||||
private HandlerThread mPositionVelocityCalculationHandlerThread; |
||||
private Handler mMyPositionVelocityCalculationHandler; |
||||
private int mCurrentColor = Color.rgb(0x4a, 0x5f, 0x70); |
||||
private int mCurrentColorIndex = 0; |
||||
private boolean mAllowShowingRawResults = false; |
||||
private MapFragment mMapFragement; |
||||
private MainActivity mMainActivity; |
||||
private PlotFragment mPlotFragment; |
||||
private int[] mRgbColorArray = { |
||||
Color.rgb(0x4a, 0x5f, 0x70), |
||||
Color.rgb(0x7f, 0x82, 0x5f), |
||||
Color.rgb(0xbf, 0x90, 0x76), |
||||
Color.rgb(0x82, 0x4e, 0x4e), |
||||
Color.rgb(0x66, 0x77, 0x7d) |
||||
}; |
||||
private int mResidualPlotStatus; |
||||
private double[] mGroundTruth = null; |
||||
private int mPositionSolutionCount = 0; |
||||
|
||||
public RealTimePositionVelocityCalculator() { |
||||
mPositionVelocityCalculationHandlerThread = |
||||
new HandlerThread("Position From Realtime Pseudoranges"); |
||||
mPositionVelocityCalculationHandlerThread.start(); |
||||
mMyPositionVelocityCalculationHandler = |
||||
new Handler(mPositionVelocityCalculationHandlerThread.getLooper()); |
||||
|
||||
final Runnable r = |
||||
new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
try { |
||||
mPseudorangePositionVelocityFromRealTimeEvents = |
||||
new PseudorangePositionVelocityFromRealTimeEvents(); |
||||
} catch (Exception e) { |
||||
Log.e( |
||||
GnssContainer.TAG, |
||||
" Exception in constructing PseudorangePositionFromRealTimeEvents : ", |
||||
e); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
mMyPositionVelocityCalculationHandler.post(r); |
||||
} |
||||
|
||||
private UIResultComponent uiResultComponent; |
||||
|
||||
public synchronized UIResultComponent getUiResultComponent() { |
||||
return uiResultComponent; |
||||
} |
||||
|
||||
public synchronized void setUiResultComponent(UIResultComponent value) { |
||||
uiResultComponent = value; |
||||
} |
||||
|
||||
@Override |
||||
public void onProviderEnabled(String provider) {} |
||||
|
||||
@Override |
||||
public void onProviderDisabled(String provider) {} |
||||
|
||||
@Override |
||||
public void onGnssStatusChanged(GnssStatus gnssStatus) {} |
||||
|
||||
/** |
||||
* Update the reference location in {@link PseudorangePositionVelocityFromRealTimeEvents} if the |
||||
* received location is a network location. Otherwise, update the {@link ResultFragment} to |
||||
* visualize both GPS location computed by the device and the one computed from the raw data. |
||||
*/ |
||||
@Override |
||||
public void onLocationChanged(final Location location) { |
||||
if (location.getProvider().equals(LocationManager.NETWORK_PROVIDER)) { |
||||
final Runnable r = |
||||
new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
if (mPseudorangePositionVelocityFromRealTimeEvents == null) { |
||||
return; |
||||
} |
||||
try { |
||||
mPseudorangePositionVelocityFromRealTimeEvents.setReferencePosition( |
||||
(int) (location.getLatitude() * 1E7), |
||||
(int) (location.getLongitude() * 1E7), |
||||
(int) (location.getAltitude() * 1E7)); |
||||
} catch (Exception e) { |
||||
Log.e(GnssContainer.TAG, " Exception setting reference location : ", e); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
mMyPositionVelocityCalculationHandler.post(r); |
||||
|
||||
} else if (location.getProvider().equals(LocationManager.GPS_PROVIDER)) { |
||||
if (mAllowShowingRawResults) { |
||||
final Runnable r = |
||||
new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
if (mPseudorangePositionVelocityFromRealTimeEvents == null) { |
||||
return; |
||||
} |
||||
double[] posSolution = |
||||
mPseudorangePositionVelocityFromRealTimeEvents.getPositionSolutionLatLngDeg(); |
||||
double[] velSolution = |
||||
mPseudorangePositionVelocityFromRealTimeEvents.getVelocitySolutionEnuMps(); |
||||
double[] pvUncertainty = |
||||
mPseudorangePositionVelocityFromRealTimeEvents |
||||
.getPositionVelocityUncertaintyEnu(); |
||||
if (Double.isNaN(posSolution[0])) { |
||||
logPositionFromRawDataEvent("No Position Calculated Yet"); |
||||
logPositionError("And no offset calculated yet..."); |
||||
} else { |
||||
if (mResidualPlotStatus != RESIDUAL_MODE_DISABLED |
||||
&& mResidualPlotStatus != RESIDUAL_MODE_AT_INPUT_LOCATION) { |
||||
updateGroundTruth(posSolution); |
||||
} |
||||
String formattedLatDegree = new DecimalFormat("##.######").format(posSolution[0]); |
||||
String formattedLngDegree = new DecimalFormat("##.######").format(posSolution[1]); |
||||
String formattedAltMeters = new DecimalFormat("##.#").format(posSolution[2]); |
||||
logPositionFromRawDataEvent( |
||||
"latDegrees = " |
||||
+ formattedLatDegree |
||||
+ " lngDegrees = " |
||||
+ formattedLngDegree |
||||
+ "altMeters = " |
||||
+ formattedAltMeters); |
||||
String formattedVelocityEastMps = |
||||
new DecimalFormat("##.###").format(velSolution[0]); |
||||
String formattedVelocityNorthMps = |
||||
new DecimalFormat("##.###").format(velSolution[1]); |
||||
String formattedVelocityUpMps = |
||||
new DecimalFormat("##.###").format(velSolution[2]); |
||||
logVelocityFromRawDataEvent( |
||||
"Velocity East = " |
||||
+ formattedVelocityEastMps |
||||
+ "mps" |
||||
+ " Velocity North = " |
||||
+ formattedVelocityNorthMps |
||||
+ "mps" |
||||
+ "Velocity Up = " |
||||
+ formattedVelocityUpMps |
||||
+ "mps"); |
||||
|
||||
String formattedPosUncertaintyEastMeters = |
||||
new DecimalFormat("##.###").format(pvUncertainty[0]); |
||||
String formattedPosUncertaintyNorthMeters = |
||||
new DecimalFormat("##.###").format(pvUncertainty[1]); |
||||
String formattedPosUncertaintyUpMeters = |
||||
new DecimalFormat("##.###").format(pvUncertainty[2]); |
||||
logPositionUncertainty( |
||||
"East = " |
||||
+ formattedPosUncertaintyEastMeters |
||||
+ "m North = " |
||||
+ formattedPosUncertaintyNorthMeters |
||||
+ "m Up = " |
||||
+ formattedPosUncertaintyUpMeters |
||||
+ "m"); |
||||
String formattedVelUncertaintyEastMeters = |
||||
new DecimalFormat("##.###").format(pvUncertainty[3]); |
||||
String formattedVelUncertaintyNorthMeters = |
||||
new DecimalFormat("##.###").format(pvUncertainty[4]); |
||||
String formattedVelUncertaintyUpMeters = |
||||
new DecimalFormat("##.###").format(pvUncertainty[5]); |
||||
logVelocityUncertainty( |
||||
"East = " |
||||
+ formattedVelUncertaintyEastMeters |
||||
+ "mps North = " |
||||
+ formattedVelUncertaintyNorthMeters |
||||
+ "mps Up = " |
||||
+ formattedVelUncertaintyUpMeters |
||||
+ "mps"); |
||||
String formattedOffsetMeters = |
||||
new DecimalFormat("##.######") |
||||
.format( |
||||
getDistanceMeters( |
||||
location.getLatitude(), |
||||
location.getLongitude(), |
||||
posSolution[0], |
||||
posSolution[1])); |
||||
logPositionError("position offset = " + formattedOffsetMeters + " meters"); |
||||
String formattedSpeedOffsetMps = |
||||
new DecimalFormat("##.###") |
||||
.format( |
||||
Math.abs( |
||||
location.getSpeed() |
||||
- Math.sqrt( |
||||
Math.pow(velSolution[0], 2) |
||||
+ Math.pow(velSolution[1], 2)))); |
||||
logVelocityError("speed offset = " + formattedSpeedOffsetMps + " mps"); |
||||
} |
||||
logLocationEvent("onLocationChanged: " + location); |
||||
if (!Double.isNaN(posSolution[0])) { |
||||
updateMapViewWithPostions( |
||||
posSolution[0], |
||||
posSolution[1], |
||||
location.getLatitude(), |
||||
location.getLongitude(), |
||||
location.getTime()); |
||||
} else { |
||||
clearMapMarkers(); |
||||
} |
||||
} |
||||
}; |
||||
mMyPositionVelocityCalculationHandler.post(r); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private void clearMapMarkers() { |
||||
mMapFragement.clearMarkers(); |
||||
} |
||||
|
||||
private void updateMapViewWithPostions( |
||||
double latDegRaw, |
||||
double lngDegRaw, |
||||
double latDegDevice, |
||||
double lngDegDevice, |
||||
long timeMillis) { |
||||
mMapFragement.updateMapViewWithPostions( |
||||
latDegRaw, lngDegRaw, latDegDevice, lngDegDevice, timeMillis); |
||||
} |
||||
|
||||
@Override |
||||
public void onLocationStatusChanged(String provider, int status, Bundle extras) {} |
||||
|
||||
@Override |
||||
public void onGnssMeasurementsReceived(final GnssMeasurementsEvent event) { |
||||
mAllowShowingRawResults = true; |
||||
final Runnable r = |
||||
new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
mMainActivity.runOnUiThread( |
||||
new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
mPlotFragment.updateCnoTab(event); |
||||
} |
||||
}); |
||||
if (mPseudorangePositionVelocityFromRealTimeEvents == null) { |
||||
return; |
||||
} |
||||
try { |
||||
if (mResidualPlotStatus != RESIDUAL_MODE_DISABLED |
||||
&& mResidualPlotStatus != RESIDUAL_MODE_AT_INPUT_LOCATION) { |
||||
// The position at last epoch is used for the residual analysis.
|
||||
// This is happening by updating the ground truth for pseudorange before using the
|
||||
// new arriving pseudoranges to compute a new position.
|
||||
mPseudorangePositionVelocityFromRealTimeEvents |
||||
.setCorrectedResidualComputationTruthLocationLla(mGroundTruth); |
||||
} |
||||
mPseudorangePositionVelocityFromRealTimeEvents |
||||
.computePositionVelocitySolutionsFromRawMeas(event); |
||||
// Running on main thread instead of in parallel will improve the thread safety
|
||||
if (mResidualPlotStatus != RESIDUAL_MODE_DISABLED) { |
||||
mMainActivity.runOnUiThread( |
||||
new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
mPlotFragment.updatePseudorangeResidualTab( |
||||
mPseudorangePositionVelocityFromRealTimeEvents |
||||
.getPseudorangeResidualsMeters(), |
||||
TimeUnit.NANOSECONDS.toSeconds( |
||||
event.getClock().getTimeNanos())); |
||||
} |
||||
} |
||||
); |
||||
} else { |
||||
mMainActivity.runOnUiThread( |
||||
new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
// Here we create gaps when the residual plot is disabled
|
||||
mPlotFragment.updatePseudorangeResidualTab( |
||||
GpsMathOperations.createAndFillArray( |
||||
GpsNavigationMessageStore.MAX_NUMBER_OF_SATELLITES, Double.NaN), |
||||
TimeUnit.NANOSECONDS.toSeconds( |
||||
event.getClock().getTimeNanos())); |
||||
} |
||||
} |
||||
); |
||||
} |
||||
} catch (Exception e) { |
||||
e.printStackTrace(); |
||||
} |
||||
} |
||||
}; |
||||
mMyPositionVelocityCalculationHandler.post(r); |
||||
} |
||||
|
||||
@Override |
||||
public void onGnssMeasurementsStatusChanged(int status) {} |
||||
|
||||
@Override |
||||
public void onGnssNavigationMessageReceived(GnssNavigationMessage event) { |
||||
if (event.getType() == GnssNavigationMessage.TYPE_GPS_L1CA) { |
||||
mPseudorangePositionVelocityFromRealTimeEvents.parseHwNavigationMessageUpdates(event); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onGnssNavigationMessageStatusChanged(int status) {} |
||||
|
||||
@Override |
||||
public void onNmeaReceived(long l, String s) {} |
||||
|
||||
@Override |
||||
public void onListenerRegistration(String listener, boolean result) {} |
||||
|
||||
private void logEvent(String tag, String message, int color) { |
||||
String composedTag = GnssContainer.TAG + tag; |
||||
Log.d(composedTag, message); |
||||
logText(tag, message, color); |
||||
} |
||||
|
||||
private void logText(String tag, String text, int color) { |
||||
UIResultComponent component = getUiResultComponent(); |
||||
if (component != null) { |
||||
component.logTextResults(tag, text, color); |
||||
} |
||||
} |
||||
|
||||
public void logLocationEvent(String event) { |
||||
mCurrentColor = getNextColor(); |
||||
logEvent("Location", event, mCurrentColor); |
||||
} |
||||
|
||||
private void logPositionFromRawDataEvent(String event) { |
||||
logEvent("Calculated Position From Raw Data", event + "\n", mCurrentColor); |
||||
} |
||||
|
||||
private void logVelocityFromRawDataEvent(String event) { |
||||
logEvent("Calculated Velocity From Raw Data", event + "\n", mCurrentColor); |
||||
} |
||||
|
||||
private void logPositionError(String event) { |
||||
logEvent( |
||||
"Offset between the reported position and Google's WLS position based on reported " |
||||
+ "measurements", |
||||
event + "\n", |
||||
mCurrentColor); |
||||
} |
||||
|
||||
private void logVelocityError(String event) { |
||||
logEvent( |
||||
"Offset between the reported velocity and " |
||||
+ "Google's computed velocity based on reported measurements ", |
||||
event + "\n", |
||||
mCurrentColor); |
||||
} |
||||
|
||||
private void logPositionUncertainty(String event) { |
||||
logEvent("Uncertainty of the calculated position from Raw Data", event + "\n", mCurrentColor); |
||||
} |
||||
|
||||
private void logVelocityUncertainty(String event) { |
||||
logEvent("Uncertainty of the calculated velocity from Raw Data", event + "\n", mCurrentColor); |
||||
} |
||||
|
||||
private synchronized int getNextColor() { |
||||
++mCurrentColorIndex; |
||||
return mRgbColorArray[mCurrentColorIndex % mRgbColorArray.length]; |
||||
} |
||||
|
||||
/** |
||||
* Return the distance (measured along the surface of the sphere) between 2 points |
||||
*/ |
||||
public double getDistanceMeters( |
||||
double lat1Degree, double lng1Degree, double lat2Degree, double lng2Degree) { |
||||
|
||||
double deltaLatRadian = Math.toRadians(lat2Degree - lat1Degree); |
||||
double deltaLngRadian = Math.toRadians(lng2Degree - lng1Degree); |
||||
|
||||
double a = |
||||
Math.sin(deltaLatRadian / 2) * Math.sin(deltaLatRadian / 2) |
||||
+ Math.cos(Math.toRadians(lat1Degree)) |
||||
* Math.cos(Math.toRadians(lat2Degree)) |
||||
* Math.sin(deltaLngRadian / 2) |
||||
* Math.sin(deltaLngRadian / 2); |
||||
double angularDistanceRad = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); |
||||
|
||||
return EARTH_RADIUS_METERS * angularDistanceRad; |
||||
} |
||||
|
||||
/** |
||||
* Update the ground truth for pseudorange residual analysis based on the user activity. |
||||
*/ |
||||
private synchronized void updateGroundTruth(double[] posSolution) { |
||||
|
||||
// In case of switching between modes, last ground truth from previous mode will be used.
|
||||
if (mGroundTruth == null) { |
||||
// If mGroundTruth has not been initialized, we set it to be the same as position solution
|
||||
mGroundTruth = new double[] {0.0, 0.0, 0.0}; |
||||
mGroundTruth[0] = posSolution[0]; |
||||
mGroundTruth[1] = posSolution[1]; |
||||
mGroundTruth[2] = posSolution[2]; |
||||
} else if (mResidualPlotStatus == RESIDUAL_MODE_STILL) { |
||||
// If the user is standing still, we average our WLS position solution
|
||||
// Reference: https://en.wikipedia.org/wiki/Moving_average#Cumulative_moving_average
|
||||
mGroundTruth[0] = |
||||
(mGroundTruth[0] * mPositionSolutionCount + posSolution[0]) |
||||
/ (mPositionSolutionCount + 1); |
||||
mGroundTruth[1] = |
||||
(mGroundTruth[1] * mPositionSolutionCount + posSolution[1]) |
||||
/ (mPositionSolutionCount + 1); |
||||
mGroundTruth[2] = |
||||
(mGroundTruth[2] * mPositionSolutionCount + posSolution[2]) |
||||
/ (mPositionSolutionCount + 1); |
||||
mPositionSolutionCount++; |
||||
} else if (mResidualPlotStatus == RESIDUAL_MODE_DRIVING) { |
||||
// If the user is moving fast, we use single WLS position solution
|
||||
mGroundTruth[0] = posSolution[0]; |
||||
mGroundTruth[1] = posSolution[1]; |
||||
mGroundTruth[2] = posSolution[2]; |
||||
mPositionSolutionCount = 0; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Sets {@link MapFragment} for receiving WLS location update |
||||
*/ |
||||
public void setMapFragment(MapFragment mapFragement) { |
||||
this.mMapFragement = mapFragement; |
||||
} |
||||
|
||||
/** |
||||
* Sets {@link PlotFragment} for receiving Gnss measurement and residual computation results for |
||||
* plot |
||||
*/ |
||||
public void setPlotFragment(PlotFragment plotFragment) { |
||||
this.mPlotFragment = plotFragment; |
||||
} |
||||
|
||||
/** |
||||
* Sets {@link MainActivity} for running some UI tasks on UI thread |
||||
*/ |
||||
public void setMainActivity(MainActivity mainActivity) { |
||||
this.mMainActivity = mainActivity; |
||||
} |
||||
|
||||
/** |
||||
* Sets the ground truth mode in {@link PseudorangePositionVelocityFromRealTimeEvents} |
||||
* for calculating corrected pseudorange residuals, also logs the change in ResultFragment |
||||
*/ |
||||
public void setResidualPlotMode(int residualPlotStatus, double[] fixedGroundTruth) { |
||||
mResidualPlotStatus = residualPlotStatus; |
||||
if (mPseudorangePositionVelocityFromRealTimeEvents == null) { |
||||
return; |
||||
} |
||||
switch (mResidualPlotStatus) { |
||||
case RESIDUAL_MODE_DRIVING: |
||||
mPseudorangePositionVelocityFromRealTimeEvents |
||||
.setCorrectedResidualComputationTruthLocationLla(mGroundTruth); |
||||
logEvent("Residual Plot", "Mode is set to driving", mCurrentColor); |
||||
break; |
||||
|
||||
case RESIDUAL_MODE_STILL: |
||||
mPseudorangePositionVelocityFromRealTimeEvents |
||||
.setCorrectedResidualComputationTruthLocationLla(mGroundTruth); |
||||
logEvent("Residual Plot", "Mode is set to still", mCurrentColor); |
||||
break; |
||||
|
||||
case RESIDUAL_MODE_AT_INPUT_LOCATION: |
||||
mPseudorangePositionVelocityFromRealTimeEvents |
||||
.setCorrectedResidualComputationTruthLocationLla(fixedGroundTruth); |
||||
logEvent("Residual Plot", "Mode is set to fixed ground truth", mCurrentColor); |
||||
break; |
||||
|
||||
case RESIDUAL_MODE_DISABLED: |
||||
mGroundTruth = null; |
||||
mPseudorangePositionVelocityFromRealTimeEvents |
||||
.setCorrectedResidualComputationTruthLocationLla(mGroundTruth); |
||||
logEvent("Residual Plot", "Mode is set to Disabled", mCurrentColor); |
||||
break; |
||||
|
||||
default: |
||||
mPseudorangePositionVelocityFromRealTimeEvents |
||||
.setCorrectedResidualComputationTruthLocationLla(null); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onTTFFReceived(long l) {} |
||||
} |
@ -0,0 +1,143 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.android.apps.location.gps.gnsslogger; |
||||
|
||||
import android.app.Activity; |
||||
import android.app.Fragment; |
||||
import android.content.Intent; |
||||
import android.content.SharedPreferences; |
||||
import android.os.Bundle; |
||||
import android.preference.PreferenceManager; |
||||
import android.text.Editable; |
||||
import android.text.SpannableStringBuilder; |
||||
import android.text.style.ForegroundColorSpan; |
||||
import android.view.LayoutInflater; |
||||
import android.view.View; |
||||
import android.view.View.OnClickListener; |
||||
import android.view.ViewGroup; |
||||
import android.widget.Button; |
||||
import android.widget.ScrollView; |
||||
import android.widget.TextView; |
||||
|
||||
/** The UI fragment that hosts a logging view. */ |
||||
public class ResultFragment extends Fragment { |
||||
|
||||
private TextView mLogView; |
||||
private ScrollView mScrollView; |
||||
|
||||
private RealTimePositionVelocityCalculator mPositionVelocityCalculator; |
||||
|
||||
private final UIResultComponent mUiComponent = new UIResultComponent(); |
||||
|
||||
public void setPositionVelocityCalculator(RealTimePositionVelocityCalculator value) { |
||||
mPositionVelocityCalculator = value; |
||||
} |
||||
|
||||
@Override |
||||
public View onCreateView( |
||||
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { |
||||
View newView = inflater.inflate(R.layout.results_log, container, false /* attachToRoot */); |
||||
mLogView = (TextView) newView.findViewById(R.id.log_view); |
||||
mScrollView = (ScrollView) newView.findViewById(R.id.log_scroll); |
||||
|
||||
RealTimePositionVelocityCalculator currentPositionVelocityCalculator = |
||||
mPositionVelocityCalculator; |
||||
if (currentPositionVelocityCalculator != null) { |
||||
currentPositionVelocityCalculator.setUiResultComponent(mUiComponent); |
||||
} |
||||
|
||||
Button start = (Button) newView.findViewById(R.id.start_log); |
||||
start.setOnClickListener( |
||||
new OnClickListener() { |
||||
@Override |
||||
public void onClick(View view) { |
||||
mScrollView.fullScroll(View.FOCUS_UP); |
||||
} |
||||
}); |
||||
|
||||
Button end = (Button) newView.findViewById(R.id.end_log); |
||||
end.setOnClickListener( |
||||
new OnClickListener() { |
||||
@Override |
||||
public void onClick(View view) { |
||||
mScrollView.fullScroll(View.FOCUS_DOWN); |
||||
} |
||||
}); |
||||
|
||||
Button clear = (Button) newView.findViewById(R.id.clear_log); |
||||
clear.setOnClickListener( |
||||
new OnClickListener() { |
||||
@Override |
||||
public void onClick(View view) { |
||||
mLogView.setText(""); |
||||
} |
||||
}); |
||||
return newView; |
||||
} |
||||
|
||||
/** |
||||
* A facade for UI and Activity related operations that are required for {@link GnssListener}s. |
||||
*/ |
||||
public class UIResultComponent { |
||||
|
||||
private static final int MAX_LENGTH = 12000; |
||||
private static final int LOWER_THRESHOLD = (int) (MAX_LENGTH * 0.5); |
||||
|
||||
public synchronized void logTextResults(final String tag, final String text, int color) { |
||||
final SpannableStringBuilder builder = new SpannableStringBuilder(); |
||||
builder.append(tag).append(" | ").append(text).append("\n"); |
||||
|
||||
builder.setSpan( |
||||
new ForegroundColorSpan(color), |
||||
0 /* start */, |
||||
builder.length(), |
||||
SpannableStringBuilder.SPAN_INCLUSIVE_EXCLUSIVE); |
||||
|
||||
Activity activity = getActivity(); |
||||
if (activity == null) { |
||||
return; |
||||
} |
||||
activity.runOnUiThread( |
||||
new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
mLogView.append(builder); |
||||
SharedPreferences sharedPreferences = PreferenceManager. |
||||
getDefaultSharedPreferences(getActivity()); |
||||
Editable editable = mLogView.getEditableText(); |
||||
int length = editable.length(); |
||||
if (length > MAX_LENGTH) { |
||||
editable.delete(0, length - LOWER_THRESHOLD); |
||||
} |
||||
if (sharedPreferences.getBoolean( |
||||
SettingsFragment.PREFERENCE_KEY_AUTO_SCROLL, false /*default return value*/)){ |
||||
mScrollView.post(new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
mScrollView.fullScroll(View.FOCUS_DOWN); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
|
||||
public void startActivity(Intent intent) { |
||||
getActivity().startActivity(intent); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,99 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.android.apps.location.gps.gnsslogger; |
||||
|
||||
import static com.google.common.base.Preconditions.checkState; |
||||
|
||||
import android.app.Activity; |
||||
import android.app.AlertDialog; |
||||
import android.app.Dialog; |
||||
import android.app.DialogFragment; |
||||
import android.content.DialogInterface; |
||||
import android.os.Bundle; |
||||
import android.view.View; |
||||
import android.widget.NumberPicker; |
||||
import com.google.android.apps.location.gps.gnsslogger.TimerService.TimerListener; |
||||
|
||||
/** A {@link Dialog} allowing "Hours", "Minutes", and "Seconds" to be selected for a timer */ |
||||
public class TimerFragment extends DialogFragment { |
||||
private TimerListener mListener; |
||||
|
||||
@Override |
||||
public void onAttach(Activity activity) { |
||||
super.onAttach(activity); |
||||
|
||||
checkState( |
||||
getTargetFragment() instanceof TimerListener, |
||||
"Target fragment is not instance of TimerListener"); |
||||
|
||||
mListener = (TimerListener) getTargetFragment(); |
||||
} |
||||
|
||||
@Override |
||||
public Dialog onCreateDialog(Bundle savedInstanceState) { |
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); |
||||
|
||||
View view = getActivity().getLayoutInflater().inflate(R.layout.timer, null); |
||||
final NumberPicker timerHours = (NumberPicker) view.findViewById(R.id.hours_picker); |
||||
final NumberPicker timerMinutes = (NumberPicker) view.findViewById(R.id.minutes_picker); |
||||
final NumberPicker timerSeconds = (NumberPicker) view.findViewById(R.id.seconds_picker); |
||||
|
||||
final TimerValues values; |
||||
|
||||
if (getArguments() != null) { |
||||
values = TimerValues.fromBundle(getArguments()); |
||||
} else { |
||||
values = new TimerValues(0 /* hours */, 0 /* minutes */, 0 /* seconds */); |
||||
} |
||||
|
||||
values.configureHours(timerHours); |
||||
values.configureMinutes(timerMinutes); |
||||
values.configureSeconds(timerSeconds); |
||||
|
||||
builder.setTitle(R.string.timer_title); |
||||
builder.setView(view); |
||||
builder.setPositiveButton( |
||||
R.string.timer_set, |
||||
new DialogInterface.OnClickListener() { |
||||
@Override |
||||
public void onClick(DialogInterface dialog, int id) { |
||||
mListener.processTimerValues( |
||||
new TimerValues( |
||||
timerHours.getValue(), timerMinutes.getValue(), timerSeconds.getValue())); |
||||
} |
||||
}); |
||||
builder.setNeutralButton( |
||||
R.string.timer_cancel, |
||||
new DialogInterface.OnClickListener() { |
||||
@Override |
||||
public void onClick(DialogInterface dialog, int id) { |
||||
mListener.processTimerValues(values); |
||||
} |
||||
}); |
||||
builder.setNegativeButton( |
||||
R.string.timer_reset, |
||||
new DialogInterface.OnClickListener() { |
||||
@Override |
||||
public void onClick(DialogInterface dialog, int id) { |
||||
mListener.processTimerValues( |
||||
new TimerValues(0 /* hours */, 0 /* minutes */, 0 /* seconds */)); |
||||
} |
||||
}); |
||||
|
||||
return builder.create(); |
||||
} |
||||
} |
@ -0,0 +1,119 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.android.apps.location.gps.gnsslogger; |
||||
|
||||
import android.app.Notification; |
||||
import android.app.Service; |
||||
import android.content.Intent; |
||||
import android.os.Binder; |
||||
import android.os.CountDownTimer; |
||||
import android.os.IBinder; |
||||
import android.support.v4.content.LocalBroadcastManager; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
/** A {@link Service} to be bound to that exposes a timer. */ |
||||
public class TimerService extends Service { |
||||
static final String TIMER_ACTION = |
||||
String.format("%s.TIMER_UPDATE", TimerService.class.getPackage().getName()); |
||||
static final String EXTRA_KEY_TYPE = "type"; |
||||
static final String EXTRA_KEY_UPDATE_REMAINING = "remaining"; |
||||
static final byte TYPE_UNKNOWN = -1; |
||||
static final byte TYPE_UPDATE = 0; |
||||
static final byte TYPE_FINISH = 1; |
||||
static final int NOTIFICATION_ID = 7777; |
||||
|
||||
private final IBinder mBinder = new TimerBinder(); |
||||
private CountDownTimer mCountDownTimer; |
||||
private boolean mTimerStarted; |
||||
|
||||
/** Handles response from {@link TimerFragment} */ |
||||
public interface TimerListener { |
||||
/** |
||||
* Process a {@link TimerValues} result |
||||
* |
||||
* @param values The set {@link TimerValues} |
||||
*/ |
||||
public void processTimerValues(TimerValues values); |
||||
} |
||||
|
||||
/** A {@link Binder} that exposes a {@link TimerService}. */ |
||||
public class TimerBinder extends Binder { |
||||
TimerService getService() { |
||||
return TimerService.this; |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onCreate() { |
||||
mTimerStarted = false; |
||||
} |
||||
|
||||
@Override |
||||
public IBinder onBind(Intent intent) { |
||||
Notification notification = new Notification(); |
||||
startForeground(NOTIFICATION_ID, notification); |
||||
return mBinder; |
||||
} |
||||
|
||||
@Override |
||||
public void onDestroy() { |
||||
if (mCountDownTimer != null) { |
||||
mCountDownTimer.cancel(); |
||||
} |
||||
mTimerStarted = false; |
||||
} |
||||
|
||||
void setTimer(TimerValues values) { |
||||
// Only allow setting when not already running
|
||||
if (!mTimerStarted) { |
||||
mCountDownTimer = |
||||
new CountDownTimer( |
||||
values.getTotalMilliseconds(), |
||||
TimeUnit.MILLISECONDS.convert(1, TimeUnit.SECONDS) /* countDownInterval */) { |
||||
@Override |
||||
public void onTick(long millisUntilFinished) { |
||||
Intent broadcast = new Intent(TIMER_ACTION); |
||||
broadcast.putExtra(EXTRA_KEY_TYPE, TYPE_UPDATE); |
||||
broadcast.putExtra(EXTRA_KEY_UPDATE_REMAINING, millisUntilFinished); |
||||
LocalBroadcastManager.getInstance(TimerService.this).sendBroadcast(broadcast); |
||||
} |
||||
|
||||
@Override |
||||
public void onFinish() { |
||||
mTimerStarted = false; |
||||
Intent broadcast = new Intent(TIMER_ACTION); |
||||
broadcast.putExtra(EXTRA_KEY_TYPE, TYPE_FINISH); |
||||
LocalBroadcastManager.getInstance(TimerService.this).sendBroadcast(broadcast); |
||||
} |
||||
}; |
||||
} |
||||
} |
||||
|
||||
void startTimer() { |
||||
if ((mCountDownTimer != null) && !mTimerStarted) { |
||||
mCountDownTimer.start(); |
||||
mTimerStarted = true; |
||||
} |
||||
} |
||||
|
||||
void stopTimer() { |
||||
if (mCountDownTimer != null) { |
||||
mCountDownTimer.cancel(); |
||||
mTimerStarted = false; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,153 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.android.apps.location.gps.gnsslogger; |
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument; |
||||
|
||||
import android.os.Bundle; |
||||
import android.widget.NumberPicker; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
/** A represetation of a time as "hours:minutes:seconds" */ |
||||
public class TimerValues { |
||||
private static final String EMPTY = "N/A"; |
||||
private static final String HOURS = "hours"; |
||||
private static final String MINUTES = "minutes"; |
||||
private static final String SECONDS = "seconds"; |
||||
private int mHours; |
||||
private int mMinutes; |
||||
private int mSeconds; |
||||
|
||||
/** |
||||
* Creates a {@link TimerValues} |
||||
* |
||||
* @param hours The number of hours to represent |
||||
* @param minutes The number of minutes to represent |
||||
* @param seconds The number of seconds to represent |
||||
*/ |
||||
public TimerValues(int hours, int minutes, int seconds) { |
||||
checkArgument(hours >= 0, "Hours is negative: %s", hours); |
||||
checkArgument(minutes >= 0, "Minutes is negative: %s", minutes); |
||||
checkArgument(seconds >= 0, "Seconds is negative: %s", seconds); |
||||
|
||||
mHours = hours; |
||||
mMinutes = minutes; |
||||
mSeconds = seconds; |
||||
|
||||
normalizeValues(); |
||||
} |
||||
|
||||
/** |
||||
* Creates a {@link TimerValues} |
||||
* |
||||
* @param milliseconds The number of milliseconds to represent |
||||
*/ |
||||
public TimerValues(long milliseconds) { |
||||
this( |
||||
0 /* hours */, |
||||
0 /* minutes */, |
||||
(int) TimeUnit.SECONDS.convert(milliseconds, TimeUnit.MILLISECONDS)); |
||||
} |
||||
|
||||
/** Creates a {@link TimerValues} from a {@link Bundle} */ |
||||
public static TimerValues fromBundle(Bundle bundle) { |
||||
checkArgument(bundle != null, "Bundle is null"); |
||||
|
||||
return new TimerValues( |
||||
bundle.getInt(HOURS, 0), bundle.getInt(MINUTES, 0), bundle.getInt(SECONDS, 0)); |
||||
} |
||||
|
||||
/** Returns a {@link Bundle} from the {@link TimerValues} */ |
||||
public Bundle toBundle() { |
||||
Bundle content = new Bundle(); |
||||
content.putInt(HOURS, mHours); |
||||
content.putInt(MINUTES, mMinutes); |
||||
content.putInt(SECONDS, mSeconds); |
||||
|
||||
return content; |
||||
} |
||||
|
||||
/** |
||||
* Configures a {@link NumberPicker} with appropriate bounds and initial value for displaying |
||||
* "Hours" |
||||
*/ |
||||
public void configureHours(NumberPicker picker) { |
||||
picker.setMinValue(0); |
||||
picker.setMaxValue((int) TimeUnit.HOURS.convert(1, TimeUnit.DAYS) - 1); |
||||
picker.setValue(mHours); |
||||
} |
||||
|
||||
/** |
||||
* Configures a {@link NumberPicker} with appropriate bounds and initial value for displaying |
||||
* "Minutes" |
||||
*/ |
||||
public void configureMinutes(NumberPicker picker) { |
||||
picker.setMinValue(0); |
||||
picker.setMaxValue((int) TimeUnit.MINUTES.convert(1, TimeUnit.HOURS) - 1); |
||||
picker.setValue(mMinutes); |
||||
} |
||||
|
||||
/** |
||||
* Configures a {@link NumberPicker} with appropriate bounds and initial value for displaying |
||||
* "Seconds" |
||||
*/ |
||||
public void configureSeconds(NumberPicker picker) { |
||||
picker.setMinValue(0); |
||||
picker.setMaxValue((int) TimeUnit.SECONDS.convert(1, TimeUnit.MINUTES) - 1); |
||||
picker.setValue(mSeconds); |
||||
} |
||||
|
||||
/** Returns the {@link TimerValues} in milliseconds */ |
||||
public long getTotalMilliseconds() { |
||||
return (TimeUnit.MILLISECONDS.convert(mHours, TimeUnit.HOURS) |
||||
+ TimeUnit.MILLISECONDS.convert(mMinutes, TimeUnit.MINUTES) |
||||
+ TimeUnit.MILLISECONDS.convert(mSeconds, TimeUnit.SECONDS)); |
||||
} |
||||
|
||||
/** Returns {@code true} if {@link TimerValues} is zero. */ |
||||
public boolean isZero() { |
||||
return ((mHours == 0) && (mMinutes == 0) && (mSeconds == 0)); |
||||
} |
||||
|
||||
/** Returns string representation that includes "00:00:00" */ |
||||
public String toCountdownString() { |
||||
return String.format("%02d:%02d:%02d", mHours, mMinutes, mSeconds); |
||||
} |
||||
|
||||
/** Normalize seconds and minutes */ |
||||
private void normalizeValues() { |
||||
long minuteOverflow = TimeUnit.MINUTES.convert(mSeconds, TimeUnit.SECONDS); |
||||
long hourOverflow = TimeUnit.HOURS.convert(mMinutes, TimeUnit.MINUTES); |
||||
|
||||
// Apply overflow
|
||||
mMinutes += minuteOverflow; |
||||
mHours += hourOverflow; |
||||
|
||||
// Apply bounds
|
||||
mSeconds -= TimeUnit.SECONDS.convert(minuteOverflow, TimeUnit.MINUTES); |
||||
mMinutes -= TimeUnit.MINUTES.convert(hourOverflow, TimeUnit.HOURS); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
if (isZero()) { |
||||
return EMPTY; |
||||
} else { |
||||
return toCountdownString(); |
||||
} |
||||
} |
||||
} |
After Width: | Height: | Size: 1.8 KiB |
@ -0,0 +1,85 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
xmlns:tools="http://schemas.android.com/tools" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" |
||||
android:orientation="vertical" |
||||
android:paddingLeft="@dimen/activity_horizontal_margin" |
||||
android:paddingRight="@dimen/activity_horizontal_margin" |
||||
android:paddingTop="@dimen/activity_vertical_margin" |
||||
android:paddingBottom="@dimen/activity_vertical_margin" |
||||
tools:context=".MainActivity"> |
||||
|
||||
<LinearLayout |
||||
android:orientation="horizontal" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content"> |
||||
<Button |
||||
android:layout_width="0dp" |
||||
android:layout_weight="1" |
||||
android:layout_height="wrap_content" |
||||
android:text="@string/clearAgps" |
||||
android:layout_marginTop="0dp" |
||||
android:id="@+id/clearAgps" |
||||
android:singleLine="true" /> |
||||
<Button |
||||
android:layout_width="0dp" |
||||
android:layout_weight="1" |
||||
android:layout_height="wrap_content" |
||||
android:text="@string/fetchExtraData" |
||||
android:layout_marginTop="0dp" |
||||
android:id="@+id/fetchExtraData" |
||||
android:singleLine="true" /> |
||||
<Button |
||||
android:layout_width="0dp" |
||||
android:layout_weight="1" |
||||
android:layout_height="wrap_content" |
||||
android:text="@string/fetchTimeData" |
||||
android:layout_marginTop="0dp" |
||||
android:id="@+id/fetchTimeData" |
||||
android:singleLine="true" /> |
||||
</LinearLayout> |
||||
|
||||
<LinearLayout |
||||
android:orientation="horizontal" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content"> |
||||
<Button |
||||
android:layout_width="0dp" |
||||
android:layout_weight="1" |
||||
android:layout_height="wrap_content" |
||||
android:text="@string/requestSingleNlp" |
||||
android:layout_marginTop="0dp" |
||||
android:id="@+id/requestSingleNlp" |
||||
android:textSize="10.5sp" |
||||
android:singleLine="true" /> |
||||
<Button |
||||
android:id="@+id/clear_log" |
||||
android:layout_marginTop="0dp" |
||||
android:text="Clear" |
||||
android:layout_width="0dp" |
||||
android:layout_weight="1" |
||||
android:layout_height="wrap_content" /> |
||||
<Button |
||||
android:layout_width="0dp" |
||||
android:layout_weight="1" |
||||
android:layout_height="wrap_content" |
||||
android:text="@string/requestSingleGps" |
||||
android:layout_marginTop="0dp" |
||||
android:id="@+id/requestSingleGps" |
||||
android:singleLine="true" /> |
||||
</LinearLayout> |
||||
|
||||
<ScrollView |
||||
android:id="@+id/log_scroll" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="0dp" |
||||
android:layout_weight="1"> |
||||
|
||||
<TextView |
||||
android:id="@+id/log_view" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content" /> |
||||
|
||||
</ScrollView> |
||||
|
||||
</LinearLayout> |
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:orientation="vertical" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent"> |
||||
<LinearLayout |
||||
android:layout_width="match_parent" |
||||
android:layout_height="0dp" |
||||
android:orientation="vertical" |
||||
android:id="@+id/plot" |
||||
android:layout_weight="3"> |
||||
</LinearLayout> |
||||
<LinearLayout |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content" |
||||
android:orientation = "horizontal"> |
||||
<Spinner |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:gravity="start" |
||||
android:id = "@+id/constellation_spinner" |
||||
android:entries="@array/constellation_arrays"> |
||||
</Spinner> |
||||
<Spinner |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:gravity="end" |
||||
android:id = "@+id/tab_spinner" |
||||
android:entries="@array/tab_arrays"> |
||||
</Spinner> |
||||
</LinearLayout> |
||||
<TextView |
||||
android:layout_width="match_parent" |
||||
android:layout_height="0dp" |
||||
android:id="@+id/analysis" |
||||
android:text="@string/turn_on_location_measurement" |
||||
android:layout_weight="1"/> |
||||
</LinearLayout> |
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:orientation="vertical" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent"> |
||||
<com.google.android.gms.maps.MapView |
||||
android:id="@+id/map" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="0dp" |
||||
android:layout_weight="9" /> |
||||
|
||||
</LinearLayout> |
@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:orientation="vertical" |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:background="@color/background_material_light" |
||||
android:id="@+id/pop"> |
||||
<TextView |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:textSize="@dimen/abc_text_size_title_material_toolbar" |
||||
android:textColor="@color/background_material_dark" |
||||
android:text="@string/please_select_ground_truth_mode"/> |
||||
<LinearLayout |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content"> |
||||
<TextView |
||||
android:layout_width="0dp" |
||||
android:layout_weight="1" |
||||
android:layout_height="wrap_content" |
||||
android:gravity="start" |
||||
android:text="@string/ground_solution_mode"/> |
||||
<Spinner |
||||
android:layout_width="0dp" |
||||
android:layout_weight="1" |
||||
android:layout_height="wrap_content" |
||||
android:gravity="end" |
||||
android:id="@+id/residual_spinner" |
||||
android:entries="@array/residual_options"> |
||||
</Spinner> |
||||
</LinearLayout> |
||||
<EditText |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:id="@+id/latitude_input" |
||||
android:inputType="numberDecimal|numberSigned" |
||||
android:hint="@string/latitude_in_degrees"/> |
||||
<EditText |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:id="@+id/longitude_input" |
||||
android:inputType="numberDecimal|numberSigned" |
||||
android:hint="@string/longitude_in_degrees"/> |
||||
<EditText |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:id="@+id/altitude_input" |
||||
android:inputType="numberDecimal|numberSigned" |
||||
android:hint="@string/altitude_in_meters"/> |
||||
<LinearLayout |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content" |
||||
android:orientation="horizontal"> |
||||
<Button |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:id="@+id/popup_button_ok" |
||||
android:text="@string/ok"/> |
||||
<Button |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:id="@+id/popup_button_cancel" |
||||
android:text="@string/cancel"/> |
||||
</LinearLayout> |
||||
</LinearLayout> |
@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" |
||||
android:orientation="vertical" > |
||||
|
||||
<LinearLayout |
||||
android:orientation="horizontal" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content"> |
||||
|
||||
<Button |
||||
android:id="@+id/start_log" |
||||
android:text="\u2770 Start" |
||||
android:layout_width="0dp" |
||||
android:layout_weight="1" |
||||
android:layout_height="wrap_content" /> |
||||
<Button |
||||
android:id="@+id/clear_log" |
||||
android:text="Clear" |
||||
android:layout_width="0dp" |
||||
android:layout_weight="1" |
||||
android:layout_height="wrap_content" /> |
||||
<Button |
||||
android:id="@+id/end_log" |
||||
android:text="End \u2771" |
||||
android:layout_width="0dp" |
||||
android:layout_weight="1" |
||||
android:layout_height="wrap_content" /> |
||||
|
||||
</LinearLayout> |
||||
|
||||
<ScrollView |
||||
android:id="@+id/log_scroll" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="0dp" |
||||
android:layout_weight="1"> |
||||
|
||||
<TextView |
||||
android:id="@+id/log_view" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content" /> |
||||
|
||||
</ScrollView> |
||||
|
||||
</LinearLayout> |
@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:orientation="vertical" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent"> |
||||
|
||||
<!-- Center landmark --> |
||||
<RelativeLayout |
||||
android:id="@+id/minutes_container" |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:layout_centerVertical="true" |
||||
android:layout_centerHorizontal="true"> |
||||
<NumberPicker |
||||
android:id="@+id/minutes_picker" |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:layout_centerVertical="true"/> |
||||
<TextView |
||||
android:id="@+id/minutes_text" |
||||
android:text="@string/timer_minutes" |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:layout_centerHorizontal="true" |
||||
android:layout_below="@id/minutes_picker"/> |
||||
|
||||
</RelativeLayout> |
||||
|
||||
<RelativeLayout |
||||
android:id="@+id/hours_container" |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:layout_toLeftOf="@id/minutes_container"> |
||||
<NumberPicker |
||||
android:id="@+id/hours_picker" |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:layout_centerVertical="true"/> |
||||
<TextView |
||||
android:id="@+id/hours_text" |
||||
android:text="@string/timer_hours" |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:layout_centerHorizontal="true" |
||||
android:layout_below="@id/hours_picker"/> |
||||
|
||||
</RelativeLayout> |
||||
|
||||
<RelativeLayout |
||||
android:id="@+id/seconds_container" |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:layout_toRightOf="@+id/minutes_container"> |
||||
<NumberPicker |
||||
android:id="@+id/seconds_picker" |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:layout_centerVertical="true"/> |
||||
<TextView |
||||
android:id="@+id/seconds_text" |
||||
android:text="@string/timer_seconds" |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:layout_centerHorizontal="true" |
||||
android:layout_below="@+id/seconds_picker"/> |
||||
|
||||
</RelativeLayout> |
||||
|
||||
</RelativeLayout> |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 8.5 KiB |
@ -1,19 +1,82 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources> |
||||
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
||||
<string name="app_name">GnssLogger</string> |
||||
<string name="app_version">v1.4.0.0</string> |
||||
<string name="app_version">v2.0.0.0</string> |
||||
|
||||
<string name="title_settings">Settings</string> |
||||
<string name="title_log">Log</string> |
||||
<string name="title_log">Log</string> |
||||
<string name="title_offset">Position Offset</string> |
||||
<string name="title_map">Map</string> |
||||
<string name="title_agnss">AGNSS</string> |
||||
<string name="title_device">Device</string> |
||||
<integer name="google_play_services_version">8487000</integer> |
||||
<string name="title_wls">WLS</string> |
||||
<string name="title_plot">Plot</string> |
||||
|
||||
<string name="location_label">Location</string> |
||||
<string name="measurements_label">Measurements</string> |
||||
<string name="nav_msg_label">Navigation Messages</string> |
||||
<string name="gnss_status_label">GnssStatus</string> |
||||
<string name="nmea_label">Nmea</string> |
||||
<string name="auto_scroll">Auto Scroll</string> |
||||
<string name="residual_plot">Residual Plot</string> |
||||
|
||||
<string name="help">HELP</string> |
||||
<string name="exit">Exit</string> |
||||
<string name="exit">Exit</string> |
||||
<string name="clearAgps">Clear AGPS</string> |
||||
<string name="fetchExtraData">Get Xtra</string> |
||||
<string name="fetchTimeData">Inject Time</string> |
||||
<string name="requestSingleNlp">Get Network Loc.</string> |
||||
<string name="requestSingleGps">Get GPS Loc.</string> |
||||
<string name="ttff">TTFF</string> |
||||
|
||||
<string name="timer_service_name">GNSS Logger timer</string> |
||||
<string name="timer_title">Timer Settings</string> |
||||
<string name="timer_hours">Hours</string> |
||||
<string name="timer_minutes">Minutes</string> |
||||
<string name="timer_seconds">Seconds</string> |
||||
<string name="timer_set">Set</string> |
||||
<string name="timer_reset">Reset</string> |
||||
<string name="timer_cancel">Cancel</string> |
||||
<string name="timer_display">Time Remaining</string> |
||||
<string name="start_message">Starting log...</string> |
||||
<string name="stop_message">Sending file...</string> |
||||
<string name="longitude_in_degrees">Longitude in decimal degrees</string> |
||||
<string name="ground_solution_mode">Ground Truth Mode:</string> |
||||
<string name="please_select_ground_truth_mode">Please Select Ground Truth Mode</string> |
||||
<string name="latitude_in_degrees">Latitude in decimal degrees</string> |
||||
<string name="altitude_in_meters">Altitude in meters</string> |
||||
<string name="turn_on_location_measurement"> |
||||
No Measurement has been received yet. Please turn on Measurement and Location in Settings. |
||||
</string> |
||||
<string name="ok">OK</string> |
||||
<string name="cancel">Cancel</string> |
||||
<string name="current_average_hint">Current Average Of Strongest 4 Satellites: |
||||
<xliff:g id="currentAverage">%s</xliff:g></string> |
||||
<string name="history_average_hint">History Average of Strongest 4 Satellites: |
||||
<xliff:g id="historyAverage">%s</xliff:g></string> |
||||
<string name="satellite_number_sum_hint">Total Number of Visible Satellites: |
||||
<xliff:g id="totalNumber">%d</xliff:g></string> |
||||
<string-array name="constellation_arrays"> |
||||
<item>All</item> |
||||
<item>GPS</item> |
||||
<item>SBAS</item> |
||||
<item>GLONASS</item> |
||||
<item>QZSS</item> |
||||
<item>BEIDOU</item> |
||||
<item>GALILEO</item> |
||||
</string-array> |
||||
<string-array name="tab_arrays"> |
||||
<item>C/N0</item> |
||||
<item>PR Residual</item> |
||||
</string-array> |
||||
<string-array name="residual_options"> |
||||
<item>Manual - Still</item> |
||||
<item>Manual - Driving</item> |
||||
<item>Manual - Use LLA input</item> |
||||
<item>Automatic - AR Based</item> |
||||
</string-array> |
||||
<string-array name="plot_titles"> |
||||
<item>CN0(dB.Hz) vs Time(s)</item> |
||||
<item>Pseudorange residual(m) vs Time(s)</item> |
||||
</string-array> |
||||
</resources> |
||||
|
@ -1,10 +1,12 @@
|
||||
## This file is automatically generated by Android Studio. |
||||
# Do not modify this file -- YOUR CHANGES WILL BE ERASED! |
||||
# |
||||
# This file should *NOT* be checked into Version Control Systems, |
||||
# This file must *NOT* be checked into Version Control Systems, |
||||
# as it contains information specific to your local configuration. |
||||
# |
||||
# Location of the SDK. This is only used by Gradle. |
||||
# For customization when using a Version Control System, please read the |
||||
# header note. |
||||
sdk.dir=/media/build/master/prebuilts/fullsdk/linux |
||||
#Mon Jul 31 15:20:15 PDT 2017 |
||||
ndk.dir=/usr/local/google/home/seangao/Android/Sdk/ndk-bundle |
||||
sdk.dir=/usr/local/google/home/seangao/Android/Sdk |
||||
|
@ -0,0 +1,53 @@
|
||||
apply plugin: 'com.android.library' |
||||
|
||||
android { |
||||
compileSdkVersion 26 |
||||
buildToolsVersion "26.0.0" |
||||
useLibrary 'org.apache.http.legacy' |
||||
defaultConfig { |
||||
minSdkVersion 24 |
||||
targetSdkVersion 25 |
||||
versionCode 1 |
||||
versionName "1.0" |
||||
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" |
||||
} |
||||
compileOptions { |
||||
sourceCompatibility JavaVersion.VERSION_1_7 |
||||
targetCompatibility JavaVersion.VERSION_1_7 |
||||
} |
||||
dexOptions { |
||||
preDexLibraries = false |
||||
javaMaxHeapSize "4g" // 2g should be also OK |
||||
} |
||||
|
||||
packagingOptions { |
||||
exclude 'META-INF/DEPENDENCIES.txt' |
||||
exclude 'META-INF/LICENSE.txt' |
||||
exclude 'META-INF/NOTICE.txt' |
||||
exclude 'META-INF/NOTICE' |
||||
exclude 'META-INF/LICENSE' |
||||
exclude 'META-INF/DEPENDENCIES' |
||||
exclude 'META-INF/notice.txt' |
||||
exclude 'META-INF/license.txt' |
||||
exclude 'META-INF/dependencies.txt' |
||||
exclude 'META-INF/LGPL2.1' |
||||
} |
||||
} |
||||
|
||||
dependencies { |
||||
compile fileTree(include: ['*.jar'], dir: 'libs') |
||||
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { |
||||
exclude group: 'com.android.support', module: 'support-annotations' |
||||
}) |
||||
compile 'com.android.support:appcompat-v7:26.+' |
||||
testCompile 'junit:junit:4.12' |
||||
compile files('libs/guava-22.0-android.jar') |
||||
compile files('libs/commons-math3-3.6.1.jar') |
||||
compile 'com.google.android.gms:play-services-location:11.0.2' |
||||
compile files('libs/commons-codec-1.10.jar') |
||||
compile files('libs/asn1-supl2.jar') |
||||
compile files('libs/asn1-base.jar') |
||||
compile files('libs/suplClient.jar') |
||||
compile files('libs/protobuf-nano.jar') |
||||
} |
@ -0,0 +1,4 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
||||
package="com.google.location.lbs.gnss.gps.pseudorange"> |
||||
|
||||
</manifest> |
@ -0,0 +1,119 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.location.lbs.gnss.gps.pseudorange; |
||||
|
||||
import org.apache.commons.math3.linear.Array2DRowRealMatrix; |
||||
import org.apache.commons.math3.linear.RealMatrix; |
||||
|
||||
/** |
||||
* Converts ECEF (Earth Centered Earth Fixed) Cartesian coordinates to local ENU (East, North, |
||||
* and Up). |
||||
* |
||||
* <p> Source: reference from Navipedia: |
||||
* http://www.navipedia.net/index.php/Transformations_between_ECEF_and_ENU_coordinates
|
||||
*/ |
||||
|
||||
public class Ecef2EnuConverter { |
||||
|
||||
/** |
||||
* Converts a vector represented by coordinates ecefX, ecefY, ecefZ in an |
||||
* Earth-Centered Earth-Fixed (ECEF) Cartesian system into a vector in a |
||||
* local east-north-up (ENU) Cartesian system. |
||||
* |
||||
* <p> For example it can be used to rotate a speed vector or position offset vector to ENU. |
||||
* |
||||
* @param ecefX X coordinates in ECEF |
||||
* @param ecefY Y coordinates in ECEF |
||||
* @param ecefZ Z coordinates in ECEF |
||||
* @param refLat Latitude in Radians of the Reference Position |
||||
* @param refLng Longitude in Radians of the Reference Position |
||||
* @return the converted values in {@code EnuValues} |
||||
*/ |
||||
public static EnuValues convertEcefToEnu(double ecefX, double ecefY, double ecefZ, |
||||
double refLat, double refLng){ |
||||
|
||||
RealMatrix rotationMatrix = getRotationMatrix(refLat, refLng); |
||||
RealMatrix ecefCoordinates = new Array2DRowRealMatrix(new double[]{ecefX, ecefY, ecefZ}); |
||||
|
||||
RealMatrix enuResult = rotationMatrix.multiply(ecefCoordinates); |
||||
return new EnuValues(enuResult.getEntry(0, 0), |
||||
enuResult.getEntry(1, 0), enuResult.getEntry(2 , 0)); |
||||
} |
||||
|
||||
/** |
||||
* Computes a rotation matrix for converting a vector in Earth-Centered Earth-Fixed (ECEF) |
||||
* Cartesian system into a vector in local east-north-up (ENU) Cartesian system with respect to |
||||
* a reference location. The matrix has the following content: |
||||
* |
||||
* - sinLng cosLng 0 |
||||
* - sinLat * cosLng - sinLat * sinLng cosLat |
||||
* cosLat * cosLng cosLat * sinLng sinLat |
||||
* |
||||
* <p> Reference: Pratap Misra and Per Enge |
||||
* "Global Positioning System: Signals, Measurements, and Performance" Page 137. |
||||
* |
||||
* @param refLat Latitude of reference location |
||||
* @param refLng Longitude of reference location |
||||
* @return the Ecef to Enu rotation matrix |
||||
*/ |
||||
public static RealMatrix getRotationMatrix(double refLat, double refLng){ |
||||
RealMatrix rotationMatrix = new Array2DRowRealMatrix(3, 3); |
||||
|
||||
// Fill in the rotation Matrix
|
||||
rotationMatrix.setEntry(0, 0, -1 * Math.sin(refLng)); |
||||
rotationMatrix.setEntry(1, 0, -1 * Math.cos(refLng) * Math.sin(refLat)); |
||||
rotationMatrix.setEntry(2, 0, Math.cos(refLng) * Math.cos(refLat)); |
||||
rotationMatrix.setEntry(0, 1, Math.cos(refLng)); |
||||
rotationMatrix.setEntry(1, 1, -1 * Math.sin(refLat) * Math.sin(refLng)); |
||||
rotationMatrix.setEntry(2, 1, Math.cos(refLat) * Math.sin(refLng)); |
||||
rotationMatrix.setEntry(0, 2, 0); |
||||
rotationMatrix.setEntry(1, 2, Math.cos(refLat)); |
||||
rotationMatrix.setEntry(2, 2, Math.sin(refLat)); |
||||
return rotationMatrix; |
||||
} |
||||
|
||||
/** |
||||
* A container for values in ENU (East, North, Up) coordination system. |
||||
*/ |
||||
public static class EnuValues { |
||||
|
||||
/** |
||||
* East Coordinates in local ENU |
||||
*/ |
||||
public final double enuEast; |
||||
|
||||
/** |
||||
* North Coordinates in local ENU |
||||
*/ |
||||
public final double enuNorth; |
||||
|
||||
/** |
||||
* Up Coordinates in local ENU |
||||
*/ |
||||
public final double enuUP; |
||||
|
||||
/** |
||||
* Constructor |
||||
*/ |
||||
public EnuValues(double enuEast, double enuNorth, double enuUP){ |
||||
this.enuEast = enuEast; |
||||
this.enuNorth = enuNorth; |
||||
this.enuUP = enuUP; |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,177 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.location.lbs.gnss.gps.pseudorange; |
||||
|
||||
/** |
||||
* Converts ECEF (Earth Centered Earth Fixed) Cartesian coordinates to LLA (latitude, longitude, |
||||
* and altitude). |
||||
* |
||||
* <p> Source: reference from Mathworks: https://microem.ru/files/2012/08/GPS.G1-X-00006.pdf
|
||||
* http://www.mathworks.com/help/aeroblks/ecefpositiontolla.html
|
||||
*/ |
||||
|
||||
public class Ecef2LlaConverter { |
||||
// WGS84 Ellipsoid Parameters
|
||||
private static final double EARTH_SEMI_MAJOR_AXIS_METERS = 6378137.0; |
||||
private static final double ECCENTRICITY = 8.1819190842622e-2; |
||||
private static final double INVERSE_FLATENNING = 298.257223563; |
||||
private static final double MIN_MAGNITUDE_METERS = 1.0e-22; |
||||
private static final double MAX_ITERATIONS = 15; |
||||
private static final double RESIDUAL_TOLERANCE = 1.0e-6; |
||||
private static final double SEMI_MINOR_AXIS_METERS = |
||||
Math.sqrt(Math.pow(EARTH_SEMI_MAJOR_AXIS_METERS, 2) * (1 - Math.pow(ECCENTRICITY, 2))); |
||||
private static final double SECOND_ECCENTRICITY = Math.sqrt( |
||||
(Math.pow(EARTH_SEMI_MAJOR_AXIS_METERS, 2) - Math.pow(SEMI_MINOR_AXIS_METERS, 2)) |
||||
/ Math.pow(SEMI_MINOR_AXIS_METERS, 2)); |
||||
private static final double ECEF_NEAR_POLE_THRESHOLD_METERS = 1.0; |
||||
|
||||
/** |
||||
* Converts ECEF (Earth Centered Earth Fixed) Cartesian coordinates to LLA (latitude, |
||||
* longitude, and altitude) using the close form approach |
||||
* |
||||
* <p>Inputs are cartesian coordinates x,y,z |
||||
* |
||||
* <p>Output is GeodeticLlaValues class containing geodetic latitude (radians), geodetic longitude |
||||
* (radians), height above WGS84 ellipsoid (m)} |
||||
*/ |
||||
public static GeodeticLlaValues convertECEFToLLACloseForm(double ecefXMeters, double ecefYMeters, |
||||
double ecefZMeters) { |
||||
|
||||
// Auxiliary parameters
|
||||
double pMeters = Math.sqrt(Math.pow(ecefXMeters, 2) + Math.pow(ecefYMeters, 2)); |
||||
double thetaRadians = |
||||
Math.atan2(EARTH_SEMI_MAJOR_AXIS_METERS * ecefZMeters, SEMI_MINOR_AXIS_METERS * pMeters); |
||||
|
||||
double lngRadians = Math.atan2(ecefYMeters, ecefXMeters); |
||||
// limit longitude to range of 0 to 2Pi
|
||||
lngRadians = lngRadians % (2 * Math.PI); |
||||
|
||||
final double sinTheta = Math.sin(thetaRadians); |
||||
final double cosTheta = Math.cos(thetaRadians); |
||||
final double tempY = ecefZMeters |
||||
+ Math.pow(SECOND_ECCENTRICITY, 2) * SEMI_MINOR_AXIS_METERS * Math.pow(sinTheta, 3); |
||||
final double tempX = pMeters |
||||
- Math.pow(ECCENTRICITY, 2) * EARTH_SEMI_MAJOR_AXIS_METERS * (Math.pow(cosTheta, 3)); |
||||
double latRadians = Math.atan2(tempY, tempX); |
||||
// Radius of curvature in the vertical prime
|
||||
double curvatureRadius = EARTH_SEMI_MAJOR_AXIS_METERS |
||||
/ Math.sqrt(1 - Math.pow(ECCENTRICITY, 2) * (Math.pow(Math.sin(latRadians), 2))); |
||||
double altMeters = (pMeters / Math.cos(latRadians)) - curvatureRadius; |
||||
|
||||
// Correct for numerical instability in altitude near poles
|
||||
boolean polesCheck = Math.abs(ecefXMeters) < ECEF_NEAR_POLE_THRESHOLD_METERS |
||||
&& Math.abs(ecefYMeters) < ECEF_NEAR_POLE_THRESHOLD_METERS; |
||||
if (polesCheck) { |
||||
altMeters = Math.abs(ecefZMeters) - SEMI_MINOR_AXIS_METERS; |
||||
} |
||||
|
||||
return new GeodeticLlaValues(latRadians, lngRadians, altMeters); |
||||
} |
||||
|
||||
/** |
||||
* Converts ECEF (Earth Centered Earth Fixed) Cartesian coordinates to LLA (latitude, |
||||
* longitude, and altitude) using iteration approach |
||||
* |
||||
* <p>Inputs are cartesian coordinates x,y,z. |
||||
* |
||||
* <p>Outputs is GeodeticLlaValues containing geodetic latitude (radians), geodetic longitude |
||||
* (radians), height above WGS84 ellipsoid (m)} |
||||
*/ |
||||
public static GeodeticLlaValues convertECEFToLLAByIterations(double ecefXMeters, |
||||
double ecefYMeters, double ecefZMeters) { |
||||
|
||||
double xyLengthMeters = Math.sqrt(Math.pow(ecefXMeters, 2) + Math.pow(ecefYMeters, 2)); |
||||
double xyzLengthMeters = Math.sqrt(Math.pow(xyLengthMeters, 2) + Math.pow(ecefZMeters, 2)); |
||||
|
||||
double lngRad; |
||||
if (xyLengthMeters > MIN_MAGNITUDE_METERS) { |
||||
lngRad = Math.atan2(ecefYMeters, ecefXMeters); |
||||
} else { |
||||
lngRad = 0; |
||||
} |
||||
|
||||
double sinPhi; |
||||
if (xyzLengthMeters > MIN_MAGNITUDE_METERS) { |
||||
sinPhi = ecefZMeters / xyzLengthMeters; |
||||
} else { |
||||
sinPhi = 0; |
||||
} |
||||
// initial latitude (iterate next to improve accuracy)
|
||||
double latRad = Math.asin(sinPhi); |
||||
double altMeters; |
||||
if (xyzLengthMeters > MIN_MAGNITUDE_METERS) { |
||||
double ni; |
||||
double pResidual; |
||||
double ecefZMetersResidual; |
||||
// initial height (iterate next to improve accuracy)
|
||||
altMeters = xyzLengthMeters - EARTH_SEMI_MAJOR_AXIS_METERS |
||||
* (1 - sinPhi * sinPhi / INVERSE_FLATENNING); |
||||
|
||||
for (int i = 1; i <= MAX_ITERATIONS; i++) { |
||||
sinPhi = Math.sin(latRad); |
||||
|
||||
// calculate radius of curvature in prime vertical direction
|
||||
ni = EARTH_SEMI_MAJOR_AXIS_METERS / Math.sqrt(1 - (2 - 1 / INVERSE_FLATENNING) |
||||
/ INVERSE_FLATENNING * Math.sin(latRad) * Math.sin(latRad)); |
||||
|
||||
// calculate residuals in p and ecefZMeters
|
||||
pResidual = xyLengthMeters - (ni + altMeters) * Math.cos(latRad); |
||||
ecefZMetersResidual = ecefZMeters |
||||
- (ni * (1 - (2 - 1 / INVERSE_FLATENNING) / INVERSE_FLATENNING) + altMeters) |
||||
* Math.sin(latRad); |
||||
|
||||
// update height and latitude
|
||||
altMeters += Math.sin(latRad) * ecefZMetersResidual + Math.cos(latRad) * pResidual; |
||||
latRad += (Math.cos(latRad) * ecefZMetersResidual - Math.sin(latRad) * pResidual) |
||||
/ (ni + altMeters); |
||||
|
||||
if (Math.sqrt((pResidual * pResidual + ecefZMetersResidual * ecefZMetersResidual)) |
||||
< RESIDUAL_TOLERANCE) { |
||||
break; |
||||
} |
||||
|
||||
if (i == MAX_ITERATIONS) { |
||||
System.err.println( |
||||
"Geodetic coordinate calculation did not converge in " + i + " iterations"); |
||||
} |
||||
} |
||||
} else { |
||||
altMeters = 0; |
||||
} |
||||
return new GeodeticLlaValues(latRad, lngRad, altMeters); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* Class containing geodetic coordinates: latitude in radians, geodetic longitude in radians |
||||
* and altitude in meters |
||||
*/ |
||||
public static class GeodeticLlaValues { |
||||
|
||||
public final double latitudeRadians; |
||||
public final double longitudeRadians; |
||||
public final double altitudeMeters; |
||||
|
||||
public GeodeticLlaValues(double latitudeRadians, |
||||
double longitudeRadians, double altitudeMeters) { |
||||
this.latitudeRadians = latitudeRadians; |
||||
this.longitudeRadians = longitudeRadians; |
||||
this.altitudeMeters = altitudeMeters; |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,107 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.location.lbs.gnss.gps.pseudorange; |
||||
|
||||
import com.google.location.lbs.gnss.gps.pseudorange.Ecef2LlaConverter.GeodeticLlaValues; |
||||
import org.apache.commons.math3.linear.RealMatrix; |
||||
|
||||
/** |
||||
* Transformations from ECEF coordiantes to Topocentric coordinates |
||||
*/ |
||||
public class EcefToTopocentricConverter { |
||||
private static final double MIN_DISTANCE_MAGNITUDE_METERS = 1.0e-22; |
||||
private static final int EAST_IDX = 0; |
||||
private static final int NORTH_IDX = 1; |
||||
private static final int UP_IDX = 2; |
||||
|
||||
/** |
||||
* Transformation of {@code inputVectorMeters} with origin at {@code originECEFMeters} into |
||||
* topocentric coordinate system. The result is {@code TopocentricAEDValues} containing azimuth |
||||
* from north +ve clockwise, radians; elevation angle, radians; distance, vector length meters |
||||
* |
||||
* <p>Source: http://www.navipedia.net/index.php/Transformations_between_ECEF_and_ENU_coordinates
|
||||
* http://kom.aau.dk/~borre/life-l99/topocent.m
|
||||
* |
||||
*/ |
||||
public static TopocentricAEDValues convertCartesianToTopocentericRadMeters( |
||||
final double[] originECEFMeters, final double[] inputVectorMeters) { |
||||
|
||||
GeodeticLlaValues latLngAlt = Ecef2LlaConverter.convertECEFToLLACloseForm(originECEFMeters[0], |
||||
originECEFMeters[1], originECEFMeters[2]); |
||||
|
||||
RealMatrix rotationMatrix = |
||||
Ecef2EnuConverter. |
||||
getRotationMatrix(latLngAlt.latitudeRadians, latLngAlt.longitudeRadians).transpose(); |
||||
double[] eastNorthUpVectorMeters = GpsMathOperations.matrixByColVectMultiplication( |
||||
rotationMatrix.transpose().getData(), inputVectorMeters); |
||||
double eastMeters = eastNorthUpVectorMeters[EAST_IDX]; |
||||
double northMeters = eastNorthUpVectorMeters[NORTH_IDX]; |
||||
double upMeters = eastNorthUpVectorMeters[UP_IDX]; |
||||
|
||||
// calculate azimuth, elevation and height from the ENU values
|
||||
double horizontalDistanceMeters = Math.hypot(eastMeters, northMeters); |
||||
double azimuthRadians; |
||||
double elevationRadians; |
||||
|
||||
if (horizontalDistanceMeters < MIN_DISTANCE_MAGNITUDE_METERS) { |
||||
elevationRadians = Math.PI / 2.0; |
||||
azimuthRadians = 0; |
||||
} else { |
||||
elevationRadians = Math.atan2(upMeters, horizontalDistanceMeters); |
||||
azimuthRadians = Math.atan2(eastMeters, northMeters); |
||||
} |
||||
if (azimuthRadians < 0) { |
||||
azimuthRadians += 2 * Math.PI; |
||||
} |
||||
|
||||
double distanceMeters = Math.sqrt(Math.pow(inputVectorMeters[0], 2) |
||||
+ Math.pow(inputVectorMeters[1], 2) + Math.pow(inputVectorMeters[2], 2)); |
||||
return new TopocentricAEDValues(elevationRadians, azimuthRadians, distanceMeters); |
||||
} |
||||
|
||||
/** |
||||
* Calculates azimuth, elevation in radians,and distance in meters between the user position in |
||||
* ECEF meters {@code userPositionECEFMeters} and the satellite position in ECEF meters |
||||
* {@code satPositionECEFMeters} |
||||
*/ |
||||
public static TopocentricAEDValues calculateElAzDistBetween2Points( |
||||
double[] userPositionECEFMeters, double[] satPositionECEFMeters) { |
||||
|
||||
return convertCartesianToTopocentericRadMeters(userPositionECEFMeters, |
||||
GpsMathOperations.subtractTwoVectors(satPositionECEFMeters, userPositionECEFMeters)); |
||||
|
||||
} |
||||
|
||||
/** |
||||
* |
||||
* Class containing topocenter coordinates: azimuth in radians, elevation in radians, and distance |
||||
* in meters |
||||
*/ |
||||
public static class TopocentricAEDValues { |
||||
|
||||
public final double elevationRadians; |
||||
public final double azimuthRadians; |
||||
public final double distanceMeters; |
||||
|
||||
public TopocentricAEDValues(double elevationRadians, double azimuthRadians, |
||||
double distanceMeters) { |
||||
this.elevationRadians = elevationRadians; |
||||
this.azimuthRadians = azimuthRadians; |
||||
this.distanceMeters = distanceMeters; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,100 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.location.lbs.gnss.gps.pseudorange; |
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8; |
||||
|
||||
import com.google.common.base.Preconditions; |
||||
import java.io.BufferedReader; |
||||
import java.io.InputStream; |
||||
import java.io.InputStreamReader; |
||||
import java.net.HttpURLConnection; |
||||
import java.net.URL; |
||||
|
||||
/** |
||||
* A helper class to access the Google Elevation API for computing the Terrain Elevation Above Sea |
||||
* level at a given location (lat, lng). An Elevation API key is required for getting elevation |
||||
* above sea level from Google server. |
||||
* |
||||
* <p> For more information please see: |
||||
* https://developers.google.com/maps/documentation/elevation/start
|
||||
* |
||||
* <p> A key can be conveniently acquired from: |
||||
* https://developers.google.com/maps/documentation/elevation/get-api-key
|
||||
*/ |
||||
|
||||
public class ElevationApiHelper { |
||||
|
||||
private static final String ELEVATION_XML_STRING = "<elevation>"; |
||||
private static final String GOOGLE_ELEVATION_API_HTTP_ADDRESS = |
||||
"https://maps.googleapis.com/maps/api/elevation/xml?locations="; |
||||
private String elevationApiKey = ""; |
||||
|
||||
/** |
||||
* A constructor that passes the {@code elevationApiKey}. If the user pass an empty string for |
||||
* API Key, an {@code IllegalArgumentException} will be thrown. |
||||
*/ |
||||
public ElevationApiHelper(String elevationApiKey){ |
||||
// An Elevation API key must be provided for getting elevation from Google Server.
|
||||
Preconditions.checkArgument(!elevationApiKey.isEmpty()); |
||||
this.elevationApiKey = elevationApiKey; |
||||
} |
||||
|
||||
/** |
||||
* Calculates the geoid height by subtracting the elevation above sea level from the ellipsoid |
||||
* height in altitude meters. |
||||
*/ |
||||
public static double calculateGeoidHeightMeters(double altitudeMeters, |
||||
double elevationAboveSeaLevelMeters){ |
||||
return altitudeMeters - elevationAboveSeaLevelMeters; |
||||
} |
||||
|
||||
/** |
||||
* Gets elevation (height above sea level) via the Google elevation API by requesting |
||||
* elevation for a given latitude and longitude. Longitude and latitude should be in decimal |
||||
* degrees and the returned elevation will be in meters. |
||||
*/ |
||||
public double getElevationAboveSeaLevelMeters(double latitudeDegrees, |
||||
double longitudeDegrees) throws Exception{ |
||||
|
||||
String url = |
||||
GOOGLE_ELEVATION_API_HTTP_ADDRESS |
||||
+ latitudeDegrees |
||||
+ "," |
||||
+ longitudeDegrees |
||||
+ "&key=" |
||||
+ elevationApiKey; |
||||
String elevationMeters = "0.0"; |
||||
|
||||
HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection(); |
||||
InputStream content = urlConnection.getInputStream(); |
||||
BufferedReader buffer = new BufferedReader(new InputStreamReader(content, UTF_8)); |
||||
String line; |
||||
while ((line = buffer.readLine()) != null) { |
||||
line = line.trim(); |
||||
if (line.startsWith(ELEVATION_XML_STRING)) { |
||||
// read the part of the line after the opening tag <elevation>
|
||||
String substring = line.substring(ELEVATION_XML_STRING.length(), line.length()); |
||||
// read the part of the line until before the closing tag <elevation>
|
||||
elevationMeters = |
||||
substring.substring(0, substring.length() - ELEVATION_XML_STRING.length() - 1); |
||||
} |
||||
} |
||||
return Double.parseDouble(elevationMeters); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,156 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.location.lbs.gnss.gps.pseudorange; |
||||
|
||||
import java.util.Arrays; |
||||
|
||||
/** |
||||
* Helper class containing the basic vector and matrix operations used for calculating the position |
||||
* solution from pseudoranges |
||||
* |
||||
*/ |
||||
public class GpsMathOperations { |
||||
|
||||
/** |
||||
* Calculates the norm of a vector |
||||
*/ |
||||
public static double vectorNorm(double[] inputVector) { |
||||
double normSqured = 0; |
||||
for (int i = 0; i < inputVector.length; i++) { |
||||
normSqured = Math.pow(inputVector[i], 2) + normSqured; |
||||
} |
||||
|
||||
return Math.sqrt(normSqured); |
||||
} |
||||
|
||||
/** |
||||
* Subtract two vectors {@code firstVector} - {@code secondVector}. Both vectors should be of the |
||||
* same length. |
||||
*/ |
||||
public static double[] subtractTwoVectors(double[] firstVector, double[] secondVector) |
||||
throws ArithmeticException { |
||||
double[] result = new double[firstVector.length]; |
||||
if (firstVector.length != secondVector.length) { |
||||
throw new ArithmeticException("Input vectors are of different lengths"); |
||||
} |
||||
|
||||
for (int i = 0; i < firstVector.length; i++) { |
||||
result[i] = firstVector[i] - secondVector[i]; |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Multiply a matrix {@code matrix} by a column vector {@code vector} |
||||
* ({@code matrix} * {@code vector}) and return the resulting vector {@resultVector}. |
||||
* {@code matrix} and {@resultVector} dimensions must match. |
||||
*/ |
||||
public static double[] matrixByColVectMultiplication(double[][] matrix, double[] resultVector) |
||||
throws ArithmeticException { |
||||
double[] result = new double[matrix.length]; |
||||
int matrixLength = matrix.length; |
||||
int vectorLength = resultVector.length; |
||||
if (vectorLength != matrix[0].length) { |
||||
throw new ArithmeticException("Matrix and vector dimensions do not match"); |
||||
} |
||||
|
||||
for (int i = 0; i < matrixLength; i++) { |
||||
for (int j = 0; j < vectorLength; j++) { |
||||
result[i] += matrix[i][j] * resultVector[j]; |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Dot product of a raw vector {@code firstVector} and a column vector {@code secondVector}. |
||||
* Both vectors should be of the same length. |
||||
*/ |
||||
public static double dotProduct(double[] firstVector, double[] secondVector) |
||||
throws ArithmeticException { |
||||
if (firstVector.length != secondVector.length) { |
||||
throw new ArithmeticException("Input vectors are of different lengths"); |
||||
} |
||||
double result = 0; |
||||
for (int i = 0; i < firstVector.length; i++) { |
||||
result = firstVector[i] * secondVector[i] + result; |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Finds the index of max value in a vector {@code vector} filtering out NaNs, return -1 if |
||||
* the vector is empty or only contain NaNs. |
||||
*/ |
||||
public static int maxIndexOfVector(double[] vector) { |
||||
double max = Double.NEGATIVE_INFINITY; |
||||
int index = -1; |
||||
|
||||
for (int i = 0; i < vector.length; i++) { |
||||
if (!Double.isNaN(vector[i])) { |
||||
if (vector[i] > max) { |
||||
index = i; |
||||
max = vector[i]; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return index; |
||||
} |
||||
|
||||
/** |
||||
* Subtracts every element in a vector {@code vector} by a scalar {@code scalar}. We do not need |
||||
* to filter out NaN in this case because NaN subtract by a real number will still be NaN. |
||||
*/ |
||||
public static double[] subtractByScalar(double[] vector, double scalar) { |
||||
double[] result = new double[vector.length]; |
||||
|
||||
for (int i = 0; i < vector.length; i++) { |
||||
result[i] = vector[i] - scalar; |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Computes the mean value of a vector {@code vector}, filtering out NaNs. If no non-NaN exists, |
||||
* return Double.NaN {@link Double#NaN} |
||||
*/ |
||||
public static double meanOfVector(double[] vector) { |
||||
double sum = 0; |
||||
double size = 0; |
||||
|
||||
for (int i = 0; i < vector.length; i++) { |
||||
if (!Double.isNaN(vector[i])) { |
||||
sum += vector[i]; |
||||
size++; |
||||
} |
||||
} |
||||
return size == 0 ? Double.NaN : sum / size; |
||||
} |
||||
|
||||
/** Creates a numeric array of size {@code size} and fills it with the value {@code value} */ |
||||
public static double[] createAndFillArray(int size, double value) { |
||||
double[] vector = new double[size]; |
||||
|
||||
Arrays.fill(vector, value); |
||||
|
||||
return vector; |
||||
} |
||||
} |
@ -0,0 +1,68 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.location.lbs.gnss.gps.pseudorange; |
||||
|
||||
/** |
||||
* A container for the received GPS measurements for a single satellite. |
||||
* |
||||
* <p>The GPS receiver measurements includes: satellite PRN, accumulated delta range in meters, |
||||
* accumulated delta range state (boolean), pseudorange rate in meters per second, received signal |
||||
* to noise ratio dB, accumulated delta range uncertainty in meters, pseudorange rate uncertainty in |
||||
* meters per second. |
||||
*/ |
||||
class GpsMeasurement { |
||||
/** Time since GPS week start (Nano seconds) */ |
||||
public final long arrivalTimeSinceGpsWeekNs; |
||||
|
||||
/** Accumulated delta range (meters) */ |
||||
public final double accumulatedDeltaRangeMeters; |
||||
|
||||
/** Accumulated delta range state */ |
||||
public final boolean validAccumulatedDeltaRangeMeters; |
||||
|
||||
/** Pseudorange rate measurement (meters per second) */ |
||||
public final double pseudorangeRateMps; |
||||
|
||||
/** Signal to noise ratio (dB) */ |
||||
public final double signalToNoiseRatioDb; |
||||
|
||||
/** Accumulated Delta Range Uncertainty (meters) */ |
||||
public final double accumulatedDeltaRangeUncertaintyMeters; |
||||
|
||||
/** Pseudorange rate uncertainty (meter per seconds) */ |
||||
public final double pseudorangeRateUncertaintyMps; |
||||
|
||||
public GpsMeasurement(long arrivalTimeSinceGpsWeekNs, double accumulatedDeltaRangeMeters, |
||||
boolean validAccumulatedDeltaRangeMeters, double pseudorangeRateMps, |
||||
double signalToNoiseRatioDb, double accumulatedDeltaRangeUncertaintyMeters, |
||||
double pseudorangeRateUncertaintyMps) { |
||||
this.arrivalTimeSinceGpsWeekNs = arrivalTimeSinceGpsWeekNs; |
||||
this.accumulatedDeltaRangeMeters = accumulatedDeltaRangeMeters; |
||||
this.validAccumulatedDeltaRangeMeters = validAccumulatedDeltaRangeMeters; |
||||
this.pseudorangeRateMps = pseudorangeRateMps; |
||||
this.signalToNoiseRatioDb = signalToNoiseRatioDb; |
||||
this.accumulatedDeltaRangeUncertaintyMeters = accumulatedDeltaRangeUncertaintyMeters; |
||||
this.pseudorangeRateUncertaintyMps = pseudorangeRateUncertaintyMps; |
||||
} |
||||
|
||||
protected GpsMeasurement(GpsMeasurement another) { |
||||
this(another.arrivalTimeSinceGpsWeekNs, another.accumulatedDeltaRangeMeters, |
||||
another.validAccumulatedDeltaRangeMeters, another.pseudorangeRateMps, |
||||
another.signalToNoiseRatioDb, another.accumulatedDeltaRangeUncertaintyMeters, |
||||
another.pseudorangeRateUncertaintyMps); |
||||
} |
||||
} |
@ -0,0 +1,40 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.location.lbs.gnss.gps.pseudorange; |
||||
|
||||
/** |
||||
* A container for the received GPS measurements for a single satellite. |
||||
* |
||||
* <p>The container extends {@link GpsMeasurement} to additionally include |
||||
* {@link #pseudorangeMeters} and {@link #pseudorangeUncertaintyMeters}. |
||||
*/ |
||||
class GpsMeasurementWithRangeAndUncertainty extends GpsMeasurement { |
||||
|
||||
/** Pseudorange measurement (meters) */ |
||||
public final double pseudorangeMeters; |
||||
|
||||
/** Pseudorange uncertainty (meters) */ |
||||
public final double pseudorangeUncertaintyMeters; |
||||
|
||||
public GpsMeasurementWithRangeAndUncertainty(GpsMeasurement another, double pseudorangeMeters, |
||||
double pseudorangeUncertaintyMeters) { |
||||
super(another); |
||||
this.pseudorangeMeters = pseudorangeMeters; |
||||
this.pseudorangeUncertaintyMeters = pseudorangeUncertaintyMeters; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,761 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.location.lbs.gnss.gps.pseudorange; |
||||
|
||||
import com.google.common.base.Preconditions; |
||||
import android.location.cts.nano.Ephemeris.GpsEphemerisProto; |
||||
import android.location.cts.nano.Ephemeris.GpsNavMessageProto; |
||||
import android.location.cts.nano.Ephemeris.IonosphericModelProto; |
||||
import android.support.annotation.NonNull; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
/** |
||||
* A class to extract the fields of the GPS navigation message from the raw bytes received from the |
||||
* GPS receiver. |
||||
* |
||||
* <p>Raw bytes are passed by calling the method |
||||
* {@link #onNavMessageReported(byte, byte, short, byte[])} |
||||
* |
||||
* <p>A {@link GpsNavMessageProto} containing the extracted field is obtained by calling the method |
||||
* {@link #createDecodedNavMessage()} |
||||
* |
||||
* <p>References: |
||||
* http://www.gps.gov/technical/icwg/IS-GPS-200D.pdf and
|
||||
* http://www.gps.gov/technical/ps/1995-SPS-signal-specification.pdf
|
||||
* |
||||
*/ |
||||
public class GpsNavigationMessageStore { |
||||
|
||||
private static final byte IONOSPHERIC_PARAMETERS_PAGE_18_SV_ID = 56; |
||||
|
||||
private static final int WORD_SIZE_BITS = 30; |
||||
private static final int WORD_PADDING_BITS = 2; |
||||
private static final int BYTE_AS_BITS = 8; |
||||
private static final int GPS_CYCLE_WEEKS = 1024; |
||||
private static final int IODE_TO_IODC_MASK = 0xFF; |
||||
|
||||
public static final int SUBFRAME_1 = (1 << 0); |
||||
public static final int SUBFRAME_2 = (1 << 1); |
||||
public static final int SUBFRAME_3 = (1 << 2); |
||||
public static final int SUBFRAME_4 = (1 << 3); |
||||
public static final int SUBFRAME_5 = (1 << 4); |
||||
|
||||
private static final double POW_2_4 = Math.pow(2, 4); |
||||
private static final double POW_2_11 = Math.pow(2, 11); |
||||
private static final double POW_2_12 = Math.pow(2, 12); |
||||
private static final double POW_2_14 = Math.pow(2, 14); |
||||
private static final double POW_2_16 = Math.pow(2, 16); |
||||
private static final double POW_2_NEG_5 = Math.pow(2, -5); |
||||
private static final double POW_2_NEG_19 = Math.pow(2, -19); |
||||
private static final double POW_2_NEG_24 = Math.pow(2, -24); |
||||
private static final double POW_2_NEG_27 = Math.pow(2, -27); |
||||
private static final double POW_2_NEG_29 = Math.pow(2, -29); |
||||
private static final double POW_2_NEG_30 = Math.pow(2, -30); |
||||
private static final double POW_2_NEG_31 = Math.pow(2, -31); |
||||
private static final double POW_2_NEG_33 = Math.pow(2, -33); |
||||
private static final double POW_2_NEG_43 = Math.pow(2, -43); |
||||
private static final double POW_2_NEG_55 = Math.pow(2, -55); |
||||
|
||||
private static final long INTEGER_RANGE = 0xFFFFFFFFL; |
||||
// 3657 is the number of days between the unix epoch and GPS epoch as the GPS epoch started on
|
||||
// Jan 6, 1980
|
||||
private static final long GPS_EPOCH_AS_UNIX_EPOCH_MS = TimeUnit.DAYS.toMillis(3657); |
||||
// A GPS Cycle is 1024 weeks, or 7168 days
|
||||
private static final long GPS_CYCLE_MS = TimeUnit.DAYS.toMillis(7168); |
||||
|
||||
/** Maximum possible number of GPS satellites */ |
||||
public static final int MAX_NUMBER_OF_SATELLITES = 32; |
||||
|
||||
private static final int L1_CA_MESSAGE_LENGTH_BYTES = 40; |
||||
|
||||
private static final int IODC1_INDEX = 82; |
||||
private static final int IODC1_LENGTH = 2; |
||||
private static final int IODC2_INDEX = 210; |
||||
private static final int IODC2_LENGTH = 8; |
||||
private static final int WEEK_INDEX = 60; |
||||
private static final int WEEK_LENGTH = 10; |
||||
private static final int URA_INDEX = 72; |
||||
private static final int URA_LENGTH = 4; |
||||
private static final int SV_HEALTH_INDEX = 76; |
||||
private static final int SV_HEALTH_LENGTH = 6; |
||||
private static final int TGD_INDEX = 196; |
||||
private static final int TGD_LENGTH = 8; |
||||
private static final int AF2_INDEX = 240; |
||||
private static final int AF2_LENGTH = 8; |
||||
private static final int AF1_INDEX = 248; |
||||
private static final int AF1_LENGTH = 16; |
||||
private static final int AF0_INDEX = 270; |
||||
private static final int AF0_LENGTH = 22; |
||||
private static final int IODE1_INDEX = 60; |
||||
private static final int IODE_LENGTH = 8; |
||||
private static final int TOC_INDEX = 218; |
||||
private static final int TOC_LENGTH = 16; |
||||
private static final int CRS_INDEX = 68; |
||||
private static final int CRS_LENGTH = 16; |
||||
private static final int DELTA_N_INDEX = 90; |
||||
private static final int DELTA_N_LENGTH = 16; |
||||
private static final int M0_INDEX8 = 106; |
||||
private static final int M0_INDEX24 = 120; |
||||
private static final int CUC_INDEX = 150; |
||||
private static final int CUC_LENGTH = 16; |
||||
private static final int E_INDEX8 = 166; |
||||
private static final int E_INDEX24 = 180; |
||||
private static final int CUS_INDEX = 210; |
||||
private static final int CUS_LENGTH = 16; |
||||
private static final int A_INDEX8 = 226; |
||||
private static final int A_INDEX24 = 240; |
||||
private static final int TOE_INDEX = 270; |
||||
private static final int TOE_LENGTH = 16; |
||||
private static final int IODE2_INDEX = 270; |
||||
private static final int CIC_INDEX = 60; |
||||
private static final int CIC_LENGTH = 16; |
||||
private static final int O0_INDEX8 = 76; |
||||
private static final int O0_INDEX24 = 90; |
||||
private static final int O_INDEX8 = 196; |
||||
private static final int O_INDEX24 = 210; |
||||
private static final int ODOT_INDEX = 240; |
||||
private static final int ODOT_LENGTH = 24; |
||||
private static final int CIS_INDEX = 120; |
||||
private static final int CIS_LENGTH = 16; |
||||
private static final int I0_INDEX8 = 136; |
||||
private static final int I0_INDEX24 = 150; |
||||
private static final int CRC_INDEX = 180; |
||||
private static final int CRC_LENGTH = 16; |
||||
private static final int IDOT_INDEX = 278; |
||||
private static final int IDOT_LENGTH = 14; |
||||
private static final int A0_INDEX = 68; |
||||
private static final int A_B_LENGTH = 8; |
||||
private static final int A1_INDEX = 76; |
||||
private static final int A2_INDEX = 90; |
||||
private static final int A3_INDEX = 98; |
||||
private static final int B0_INDEX = 106; |
||||
private static final int B1_INDEX = 120; |
||||
private static final int B2_INDEX = 128; |
||||
private static final int B3_INDEX = 136; |
||||
private static final int WN_LS_INDEX = 226; |
||||
private static final int DELTA_T_LS_INDEX = 240; |
||||
private static final int TOT_LS_INDEX = 218; |
||||
private static final int DN_LS_INDEX = 256; |
||||
private static final int WNF_LS_INDEX = 248; |
||||
private static final int DELTA_TF_LS_INDEX = 270; |
||||
private static final int I0UTC_INDEX8 = 210; |
||||
private static final int I0UTC_INDEX24 = 180; |
||||
private static final int I1UTC_INDEX = 150; |
||||
|
||||
/** Partially decoded intermediate ephemerides */ |
||||
private final IntermediateEphemeris[] partiallyDecodedIntermediateEphemerides = |
||||
new IntermediateEphemeris[MAX_NUMBER_OF_SATELLITES]; |
||||
|
||||
/** Fully decoded intermediate ephemerides */ |
||||
private final IntermediateEphemeris[] fullyDecodedIntermediateEphemerides = |
||||
new IntermediateEphemeris[MAX_NUMBER_OF_SATELLITES]; |
||||
|
||||
|
||||
private IonosphericModelProto decodedIonosphericObj; |
||||
|
||||
/** |
||||
* Builds and returns the current {@link GpsNavMessageProto} filling the different ephemeris for |
||||
* the different satellites and setting the ionospheric model parameters. |
||||
*/ |
||||
@NonNull |
||||
public GpsNavMessageProto createDecodedNavMessage() { |
||||
synchronized (fullyDecodedIntermediateEphemerides) {; |
||||
GpsNavMessageProto gpsNavMessageProto = new GpsNavMessageProto(); |
||||
ArrayList<GpsEphemerisProto> gpsEphemerisProtoList = new ArrayList<>(); |
||||
for (int i = 0; i < MAX_NUMBER_OF_SATELLITES; i++) { |
||||
if (fullyDecodedIntermediateEphemerides[i] != null) { |
||||
gpsEphemerisProtoList.add(fullyDecodedIntermediateEphemerides[i].getEphemerisObj()); |
||||
} |
||||
} |
||||
if (decodedIonosphericObj != null) { |
||||
gpsNavMessageProto.iono = decodedIonosphericObj; |
||||
} |
||||
gpsNavMessageProto.ephemerids = |
||||
gpsEphemerisProtoList.toArray(new GpsEphemerisProto[gpsEphemerisProtoList.size()]); |
||||
return gpsNavMessageProto; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handles a fresh Navigation Message. The message is in its raw format. |
||||
*/ |
||||
public void onNavMessageReported(byte prn, byte type, short id, byte[] rawData) { |
||||
Preconditions.checkArgument(type == 1, "Unsupported NavigationMessage Type: " + type); |
||||
Preconditions.checkArgument( |
||||
rawData != null && rawData.length == L1_CA_MESSAGE_LENGTH_BYTES, |
||||
"Invalid length of rawData for L1 C/A"); |
||||
synchronized (fullyDecodedIntermediateEphemerides) { |
||||
switch (id) { |
||||
case 1: |
||||
handleFirstSubframe(prn, rawData); |
||||
break; |
||||
case 2: |
||||
handleSecondSubframe(prn, rawData); |
||||
break; |
||||
case 3: |
||||
handleThirdSubframe(prn, rawData); |
||||
break; |
||||
case 4: |
||||
handleFourthSubframe(rawData); |
||||
break; |
||||
case 5: |
||||
break; |
||||
default: |
||||
// invalid message id
|
||||
throw new IllegalArgumentException("Invalid Subframe ID: " + id); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handles the first navigation message subframe which contains satellite clock correction |
||||
* parameters, GPS date (week number) plus satellite status and health. |
||||
*/ |
||||
private void handleFirstSubframe(byte prn, byte[] rawData) { |
||||
int iodc = extractBits(IODC1_INDEX, IODC1_LENGTH, rawData) << 8; |
||||
iodc |= extractBits(IODC2_INDEX, IODC2_LENGTH, rawData); |
||||
|
||||
IntermediateEphemeris intermediateEphemeris = |
||||
findIntermediateEphemerisToUpdate(prn, SUBFRAME_1, iodc); |
||||
if (intermediateEphemeris == null) { |
||||
// we are up-to-date
|
||||
return; |
||||
} |
||||
|
||||
GpsEphemerisProto gpsEphemerisProto = intermediateEphemeris.getEphemerisObj(); |
||||
gpsEphemerisProto.iodc = iodc; |
||||
|
||||
|
||||
// the navigation message contains a modulo-1023 week number
|
||||
int week = extractBits(WEEK_INDEX, WEEK_LENGTH, rawData); |
||||
week = getGpsWeekWithRollover(week); |
||||
gpsEphemerisProto.week = week; |
||||
|
||||
int uraIndex = extractBits(URA_INDEX, URA_LENGTH, rawData); |
||||
double svAccuracy = computeNominalSvAccuracy(uraIndex); |
||||
gpsEphemerisProto.svAccuracyM = svAccuracy; |
||||
|
||||
int svHealth = extractBits(SV_HEALTH_INDEX, SV_HEALTH_LENGTH, rawData); |
||||
gpsEphemerisProto.svHealth = svHealth; |
||||
|
||||
byte tgd = (byte) extractBits(TGD_INDEX, TGD_LENGTH, rawData); |
||||
gpsEphemerisProto.tgd = tgd * POW_2_NEG_31; |
||||
|
||||
int toc = extractBits(TOC_INDEX, TOC_LENGTH, rawData); |
||||
double tocScaled = toc * POW_2_4; |
||||
gpsEphemerisProto.toc = tocScaled; |
||||
|
||||
byte af2 = (byte) extractBits(AF2_INDEX, AF2_LENGTH, rawData); |
||||
gpsEphemerisProto.af2 = af2 * POW_2_NEG_55; |
||||
|
||||
short af1 = (short) extractBits(AF1_INDEX, AF1_LENGTH, rawData); |
||||
gpsEphemerisProto.af1 = af1 * POW_2_NEG_43; |
||||
|
||||
// a 22-bit two's complement number
|
||||
int af0 = extractBits(AF0_INDEX, AF0_LENGTH, rawData); |
||||
af0 = getTwoComplement(af0, AF0_LENGTH); |
||||
gpsEphemerisProto.af0 = af0 * POW_2_NEG_31; |
||||
|
||||
updateDecodedState(prn, SUBFRAME_1, intermediateEphemeris); |
||||
} |
||||
|
||||
/** |
||||
* Handles the second navigation message subframe which contains satellite ephemeris |
||||
*/ |
||||
private void handleSecondSubframe(byte prn, byte[] rawData) { |
||||
int iode = extractBits(IODE1_INDEX, IODE_LENGTH, rawData); |
||||
|
||||
IntermediateEphemeris intermediateEphemeris = |
||||
findIntermediateEphemerisToUpdate(prn, SUBFRAME_2, iode); |
||||
if (intermediateEphemeris == null) { |
||||
// nothing to update
|
||||
return; |
||||
} |
||||
|
||||
GpsEphemerisProto gpsEphemerisProto = intermediateEphemeris.getEphemerisObj(); |
||||
|
||||
gpsEphemerisProto.iode = iode; |
||||
|
||||
short crs = (short) extractBits(CRS_INDEX, CRS_LENGTH, rawData); |
||||
gpsEphemerisProto.crc = crs * POW_2_NEG_5; |
||||
|
||||
short deltaN = (short) extractBits(DELTA_N_INDEX, DELTA_N_LENGTH, rawData); |
||||
gpsEphemerisProto.deltaN = deltaN * POW_2_NEG_43 * Math.PI; |
||||
|
||||
int m0 = (int) buildUnsigned32BitsWordFrom8And24Words(M0_INDEX8, M0_INDEX24, rawData); |
||||
gpsEphemerisProto.m0 = m0 * POW_2_NEG_31 * Math.PI; |
||||
|
||||
short cuc = (short) extractBits(CUC_INDEX, CUC_LENGTH, rawData); |
||||
gpsEphemerisProto.cuc = cuc * POW_2_NEG_29; |
||||
|
||||
// an unsigned 32 bit value
|
||||
long e = buildUnsigned32BitsWordFrom8And24Words(E_INDEX8, E_INDEX24, rawData); |
||||
gpsEphemerisProto.e = e * POW_2_NEG_33; |
||||
|
||||
short cus = (short) extractBits(CUS_INDEX, CUS_LENGTH, rawData); |
||||
gpsEphemerisProto.cus = cus * POW_2_NEG_29; |
||||
|
||||
// an unsigned 32 bit value
|
||||
long a = buildUnsigned32BitsWordFrom8And24Words(A_INDEX8, A_INDEX24, rawData); |
||||
gpsEphemerisProto.rootOfA = a * POW_2_NEG_19; |
||||
|
||||
int toe = extractBits(TOE_INDEX, TOE_LENGTH, rawData); |
||||
double toeScaled = toe * POW_2_4; |
||||
gpsEphemerisProto.toe = toe * POW_2_4; |
||||
|
||||
updateDecodedState(prn, SUBFRAME_2, intermediateEphemeris); |
||||
} |
||||
|
||||
/** |
||||
* Handles the third navigation message subframe which contains satellite ephemeris |
||||
*/ |
||||
private void handleThirdSubframe(byte prn, byte[] rawData) { |
||||
|
||||
int iode = extractBits(IODE2_INDEX, IODE_LENGTH, rawData); |
||||
|
||||
IntermediateEphemeris intermediateEphemeris = |
||||
findIntermediateEphemerisToUpdate(prn, SUBFRAME_3, iode); |
||||
if (intermediateEphemeris == null) { |
||||
// A fully or partially decoded message is available , hence nothing to update
|
||||
return; |
||||
} |
||||
|
||||
GpsEphemerisProto gpsEphemerisProto = intermediateEphemeris.getEphemerisObj(); |
||||
gpsEphemerisProto.iode = iode; |
||||
|
||||
short cic = (short) extractBits(CIC_INDEX, CIC_LENGTH, rawData); |
||||
gpsEphemerisProto.cic = cic * POW_2_NEG_29; |
||||
|
||||
int o0 = (int) buildUnsigned32BitsWordFrom8And24Words(O0_INDEX8, O0_INDEX24, rawData); |
||||
gpsEphemerisProto.omega0 = o0 * POW_2_NEG_31 * Math.PI; |
||||
|
||||
int o = (int) buildUnsigned32BitsWordFrom8And24Words(O_INDEX8, O_INDEX24, rawData); |
||||
gpsEphemerisProto.omega = o * POW_2_NEG_31 * Math.PI; |
||||
|
||||
int odot = extractBits(ODOT_INDEX, ODOT_LENGTH, rawData); |
||||
odot = getTwoComplement(odot, ODOT_LENGTH);; |
||||
gpsEphemerisProto.omegaDot = o * POW_2_NEG_43 * Math.PI; |
||||
|
||||
short cis = (short) extractBits(CIS_INDEX, CIS_LENGTH, rawData); |
||||
gpsEphemerisProto.cis = cis * POW_2_NEG_29; |
||||
|
||||
int i0 = (int) buildUnsigned32BitsWordFrom8And24Words(I0_INDEX8, I0_INDEX24, rawData); |
||||
gpsEphemerisProto.i0 = i0 * POW_2_NEG_31 * Math.PI; |
||||
|
||||
short crc = (short) extractBits(CRC_INDEX, CRC_LENGTH, rawData); |
||||
gpsEphemerisProto.crc = crc * POW_2_NEG_5; |
||||
|
||||
|
||||
// a 14-bit two's complement number
|
||||
int idot = extractBits(IDOT_INDEX, IDOT_LENGTH, rawData); |
||||
idot = getTwoComplement(idot, IDOT_LENGTH); |
||||
gpsEphemerisProto.iDot = idot * POW_2_NEG_43 * Math.PI; |
||||
|
||||
updateDecodedState(prn, SUBFRAME_3, intermediateEphemeris); |
||||
} |
||||
|
||||
/** |
||||
* Subframe four provides ionospheric model parameters , UTC information, part of the almanac, and |
||||
* indications whether the Anti-Spoofing, is activated or not. |
||||
* |
||||
* <p>For now, only the ionospheric parameters are parsed. |
||||
*/ |
||||
private void handleFourthSubframe(byte[] rawData) { |
||||
byte pageId = (byte) extractBits(62, 6, rawData); |
||||
if (pageId != IONOSPHERIC_PARAMETERS_PAGE_18_SV_ID) { |
||||
// We only care to decode ionospheric parameters for now
|
||||
return; |
||||
} |
||||
|
||||
IonosphericModelProto ionosphericModelProto = new IonosphericModelProto(); |
||||
|
||||
double[] alpha = new double[4]; |
||||
byte a0 = (byte) extractBits(A0_INDEX, A_B_LENGTH, rawData); |
||||
alpha[0] = a0 * POW_2_NEG_30; |
||||
byte a1 = (byte) extractBits(A1_INDEX, A_B_LENGTH, rawData); |
||||
alpha[1] = a1 * POW_2_NEG_27; |
||||
byte a2 = (byte) extractBits(A2_INDEX, A_B_LENGTH, rawData); |
||||
alpha[2] = a2 * POW_2_NEG_24; |
||||
byte a3 = (byte) extractBits(A3_INDEX, A_B_LENGTH, rawData); |
||||
alpha[3] = a3 * POW_2_NEG_24; |
||||
ionosphericModelProto.alpha = alpha; |
||||
|
||||
double[] beta = new double[4]; |
||||
byte b0 = (byte) extractBits(B0_INDEX, A_B_LENGTH, rawData); |
||||
beta[0] = b0 * POW_2_11; |
||||
byte b1 = (byte) extractBits(B1_INDEX, A_B_LENGTH, rawData); |
||||
beta[1] = b1 * POW_2_14; |
||||
byte b2 = (byte) extractBits(B2_INDEX, A_B_LENGTH, rawData); |
||||
beta[2] = b2 * POW_2_16; |
||||
byte b3 = (byte) extractBits(B3_INDEX, A_B_LENGTH, rawData); |
||||
beta[3] = b3 * POW_2_16; |
||||
ionosphericModelProto.beta = beta; |
||||
|
||||
|
||||
double a0UTC = |
||||
buildSigned32BitsWordFrom8And24WordsWith8bitslsb(I0UTC_INDEX8, I0UTC_INDEX24, rawData) |
||||
* Math.pow(2, -30); |
||||
|
||||
double a1UTC = getTwoComplement(extractBits(I1UTC_INDEX, 24, rawData), 24) * Math.pow(2, -50); |
||||
|
||||
short tot = (short) (extractBits(TOT_LS_INDEX, A_B_LENGTH, rawData) * POW_2_12); |
||||
|
||||
short wnt = (short) extractBits(WN_LS_INDEX, A_B_LENGTH, rawData); |
||||
|
||||
short tls = (short) extractBits(DELTA_T_LS_INDEX, A_B_LENGTH, rawData); |
||||
|
||||
short wnlsf = (short) extractBits(WNF_LS_INDEX, A_B_LENGTH, rawData); |
||||
|
||||
short dn = (short) extractBits(DN_LS_INDEX, A_B_LENGTH, rawData); |
||||
|
||||
short tlsf = (short) extractBits(DELTA_TF_LS_INDEX, A_B_LENGTH, rawData); |
||||
|
||||
decodedIonosphericObj = ionosphericModelProto; |
||||
} |
||||
|
||||
/** |
||||
* Updates the {@link IntermediateEphemeris} with the decoded status of the current subframe. |
||||
* Moreover, update the {@code partiallyDecodedIntermediateEphemerides} list and |
||||
* {@code fullyDecodedIntermediateEphemerides} list |
||||
*/ |
||||
private void updateDecodedState(byte prn, int decodedSubframeNumber, |
||||
IntermediateEphemeris intermediateEphemeris) { |
||||
intermediateEphemeris.reportDecodedSubframe(decodedSubframeNumber); |
||||
if (intermediateEphemeris.isFullyDecoded()) { |
||||
partiallyDecodedIntermediateEphemerides[prn - 1] = null; |
||||
fullyDecodedIntermediateEphemerides[prn - 1] = intermediateEphemeris; |
||||
} else { |
||||
partiallyDecodedIntermediateEphemerides[prn - 1] = intermediateEphemeris; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Extracts the requested bits from the raw stream. |
||||
* |
||||
* @param index Zero-based index of the first bit to extract. |
||||
* @param length The length of the stream of bits to extract. |
||||
* @param rawData The stream to extract data from. |
||||
* |
||||
* @return The bits requested always shifted to the least significant positions. |
||||
*/ |
||||
private static int extractBits(int index, int length, byte[] rawData) { |
||||
int result = 0; |
||||
|
||||
for (int i = 0; i < length; ++i) { |
||||
int workingIndex = index + i; |
||||
|
||||
int wordIndex = workingIndex / WORD_SIZE_BITS; |
||||
// account for 2 bit padding for every 30bit word
|
||||
workingIndex += (wordIndex + 1) * WORD_PADDING_BITS; |
||||
int byteIndex = workingIndex / BYTE_AS_BITS; |
||||
int byteOffset = workingIndex % BYTE_AS_BITS; |
||||
|
||||
byte raw = rawData[byteIndex]; |
||||
// account for zero-based indexing
|
||||
int shiftOffset = BYTE_AS_BITS - 1 - byteOffset; |
||||
int mask = 1 << shiftOffset; |
||||
int bit = raw & mask; |
||||
bit >>= shiftOffset; |
||||
|
||||
// account for zero-based indexing
|
||||
result |= bit << length - 1 - i; |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Extracts an unsigned 32 bit word where the word is partitioned 8/24 bits. |
||||
* |
||||
* @param index8 The index of the first 8 bits used. |
||||
* @param index24 The index of the last 24 bits used. |
||||
* @param rawData The stream to extract data from. |
||||
* |
||||
* @return The bits requested represented as a long and stored in the least significant positions. |
||||
*/ |
||||
private static long buildUnsigned32BitsWordFrom8And24Words(int index8, int index24, |
||||
byte[] rawData) { |
||||
long result = (long) extractBits(index8, 8, rawData) << 24; |
||||
result |= extractBits(index24, 24, rawData); |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Extracts a signed 32 bit word where the word is partitioned 8/24 bits with LSB first. |
||||
* |
||||
* @param index8 The index of the first 8 bits used. |
||||
* @param index24 The index of the last 24 bits used. |
||||
* @param rawData The stream to extract data from. |
||||
* @return The bits requested represented as an int and stored in the least significant positions. |
||||
*/ |
||||
private static int buildSigned32BitsWordFrom8And24WordsWith8bitslsb( |
||||
int index8, int index24, byte[] rawData) { |
||||
int result = extractBits(index24, 24, rawData) << 8; |
||||
result |= extractBits(index8, 8, rawData); |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Calculates the 2s complement for a specific number of bits of a given value |
||||
* |
||||
* @param value The set of bits to translate. |
||||
* @param bits The number of bits to consider. |
||||
* |
||||
* @return The calculated 2s complement. |
||||
*/ |
||||
private static int getTwoComplement(int value, int bits) { |
||||
int msbMask = 1 << bits - 1; |
||||
int msb = value & msbMask; |
||||
if (msb == 0) { |
||||
// the value is positive
|
||||
return value; |
||||
} |
||||
|
||||
int valueBitMask = (1 << bits) - 1; |
||||
int extendedSignMask = (int) INTEGER_RANGE - valueBitMask; |
||||
return value | extendedSignMask; |
||||
} |
||||
|
||||
/** |
||||
* Calculates the GPS week with rollovers. A rollover happens every 1024 weeks, beginning from GPS |
||||
* epoch (January 6, 1980). |
||||
* |
||||
* @param gpsWeek The modulo-1024 GPS week. |
||||
* |
||||
* @return The absolute GPS week. |
||||
*/ |
||||
private static int getGpsWeekWithRollover(int gpsWeek) { |
||||
long nowMs = System.currentTimeMillis(); |
||||
long elapsedTimeFromGpsEpochMs = nowMs - GPS_EPOCH_AS_UNIX_EPOCH_MS; |
||||
long rolloverCycles = elapsedTimeFromGpsEpochMs / GPS_CYCLE_MS; |
||||
int rolloverWeeks = (int) rolloverCycles * GPS_CYCLE_WEEKS; |
||||
return gpsWeek + rolloverWeeks; |
||||
} |
||||
|
||||
/** |
||||
* Computes a nominal Sv Accuracy based on the URA index. This implementation is taken from |
||||
* http://www.gps.gov/technical/icwg/IS-GPS-200D.pdf, section '20.3.3.3.1.3 Sv Accuracy'.
|
||||
* |
||||
* @param uraIndex The URA Index |
||||
* |
||||
* @return A computed nominal Sv accuracy. |
||||
*/ |
||||
private static double computeNominalSvAccuracy(int uraIndex) { |
||||
if (uraIndex < 0 || uraIndex >= 15) { |
||||
return Double.NaN; |
||||
} else if (uraIndex == 1) { |
||||
return 2.8; |
||||
} else if (uraIndex == 3) { |
||||
return 5.7; |
||||
} else if (uraIndex == 5) { |
||||
return 11.3; |
||||
} |
||||
|
||||
int exponent; |
||||
if (uraIndex < 6) { |
||||
exponent = 1 + (uraIndex / 2); |
||||
} else { |
||||
exponent = uraIndex - 2; |
||||
} |
||||
return Math.pow(2, exponent); |
||||
} |
||||
|
||||
/** |
||||
* Finds an {@link IntermediateEphemeris} that can be updated by the given data. The pseudocode is |
||||
* as follows: |
||||
* <p>if a fully decoded message is available and matches, there is no need to update |
||||
* <p>if a partially decoded message is available and matches, there is no need to update |
||||
* <p>if the provided issueOfData matches intermediate partially decoded state, update in place |
||||
* <p>otherwise, start a new decoding 'session' for the prn |
||||
* |
||||
* @param prn The prn to update |
||||
* @param subframe The subframe available to update |
||||
* @param issueOfData The issueOfData associated with the given subframe |
||||
* @return a {@link IntermediateEphemeris} to update with the available data, {@code null} if |
||||
* there is no need to update a {@link IntermediateEphemeris}. |
||||
*/ |
||||
private IntermediateEphemeris findIntermediateEphemerisToUpdate(byte prn, int subframe, |
||||
int issueOfData) { |
||||
// find out if we have fully decoded up-to-date ephemeris first
|
||||
IntermediateEphemeris fullyDecodedIntermediateEphemeris = |
||||
this.fullyDecodedIntermediateEphemerides[prn - 1]; |
||||
if (fullyDecodedIntermediateEphemeris != null |
||||
&& fullyDecodedIntermediateEphemeris.findSubframeInfo(prn, subframe, issueOfData) |
||||
.isSubframeDecoded()) { |
||||
return null; |
||||
} |
||||
|
||||
// find out next if there is a partially decoded intermediate state we can continue working on
|
||||
IntermediateEphemeris partiallyDecodedIntermediateEphemeris = |
||||
this.partiallyDecodedIntermediateEphemerides[prn - 1]; |
||||
if (partiallyDecodedIntermediateEphemeris == null) { |
||||
// no intermediate partially decoded state, we need to start a decoding 'session'
|
||||
return new IntermediateEphemeris(prn); |
||||
} |
||||
SubframeCheckResult subframeCheckResult = partiallyDecodedIntermediateEphemeris |
||||
.findSubframeInfo(prn, subframe, issueOfData); |
||||
if (subframeCheckResult.isSubframeDecoded()) { |
||||
return null; |
||||
} |
||||
if (subframeCheckResult.hasSubframe && !subframeCheckResult.issueOfDataMatches) { |
||||
// the navigation message has changed, we need to start over
|
||||
return new IntermediateEphemeris(prn); |
||||
} |
||||
|
||||
int intermediateIode = Integer.MAX_VALUE; |
||||
boolean intermediateHasIode = false; |
||||
GpsEphemerisProto gpsEphemerisProto = partiallyDecodedIntermediateEphemeris.getEphemerisObj(); |
||||
|
||||
if (partiallyDecodedIntermediateEphemeris.hasDecodedSubframe(SUBFRAME_1)) { |
||||
intermediateHasIode = true; |
||||
intermediateIode = gpsEphemerisProto.iodc & IODE_TO_IODC_MASK; |
||||
} |
||||
if (partiallyDecodedIntermediateEphemeris.hasDecodedSubframe(SUBFRAME_2) |
||||
|| partiallyDecodedIntermediateEphemeris.hasDecodedSubframe(SUBFRAME_3)) { |
||||
intermediateHasIode = true; |
||||
intermediateIode = gpsEphemerisProto.iode; |
||||
} |
||||
|
||||
boolean canContinueDecoding; |
||||
int iode; |
||||
switch (subframe) { |
||||
case SUBFRAME_1: |
||||
iode = issueOfData & IODE_TO_IODC_MASK; |
||||
canContinueDecoding = !intermediateHasIode || (intermediateIode == iode); |
||||
break; |
||||
case SUBFRAME_2: |
||||
// fall through
|
||||
case SUBFRAME_3: |
||||
iode = issueOfData; |
||||
canContinueDecoding = !intermediateHasIode || (intermediateIode == iode); |
||||
break; |
||||
case SUBFRAME_4: |
||||
// fall through
|
||||
case SUBFRAME_5: |
||||
// always continue decoding for subframes 4-5
|
||||
canContinueDecoding = true; |
||||
break; |
||||
default: |
||||
throw new IllegalStateException("invalid subframe requested: " + subframe); |
||||
} |
||||
|
||||
if (canContinueDecoding) { |
||||
return partiallyDecodedIntermediateEphemeris; |
||||
} |
||||
return new IntermediateEphemeris(prn); |
||||
} |
||||
|
||||
/** |
||||
* A representation of an intermediate ephemeris that can be fully decoded or partially decoded. |
||||
*/ |
||||
private static class IntermediateEphemeris { |
||||
|
||||
private final GpsEphemerisProto gpsEphemerisProtoObj = new GpsEphemerisProto(); |
||||
|
||||
private int subframesDecoded; |
||||
|
||||
public IntermediateEphemeris(byte prn) { |
||||
gpsEphemerisProtoObj.prn = prn; |
||||
} |
||||
|
||||
public void reportDecodedSubframe(int subframe) { |
||||
subframesDecoded |= subframe; |
||||
} |
||||
|
||||
public boolean hasDecodedSubframe(int subframe) { |
||||
return (subframesDecoded & subframe) == subframe; |
||||
} |
||||
|
||||
public boolean isFullyDecoded() { |
||||
return hasDecodedSubframe(SUBFRAME_1) && hasDecodedSubframe(SUBFRAME_2) |
||||
&& hasDecodedSubframe(SUBFRAME_3); |
||||
} |
||||
|
||||
public GpsEphemerisProto getEphemerisObj() { |
||||
return gpsEphemerisProtoObj; |
||||
} |
||||
|
||||
/** |
||||
* Verifies that the received subframe info (IODE and IODC) matches the existing info (IODE and |
||||
* IODC). For each subframe there is a given issueOfData that must match, this method abstracts |
||||
* the logic to perform such check. |
||||
* |
||||
* @param prn The expected prn. |
||||
* @param subframe The expected subframe. |
||||
* @param issueOfData The issueOfData for the given subframe. |
||||
* |
||||
* @return {@link SubframeCheckResult} representing the state found. |
||||
*/ |
||||
public SubframeCheckResult findSubframeInfo(byte prn, int subframe, int issueOfData) { |
||||
if (gpsEphemerisProtoObj.prn != prn) { |
||||
return new SubframeCheckResult(false /* hasSubframe */, false /* issueOfDataMatches */); |
||||
} |
||||
boolean issueOfDataMatches; |
||||
switch (subframe) { |
||||
case SUBFRAME_1: |
||||
issueOfDataMatches = gpsEphemerisProtoObj.iodc == issueOfData; |
||||
break; |
||||
case SUBFRAME_2: |
||||
// fall through
|
||||
case SUBFRAME_3: |
||||
issueOfDataMatches = gpsEphemerisProtoObj.iode == issueOfData; |
||||
break; |
||||
case SUBFRAME_4: |
||||
// fall through
|
||||
case SUBFRAME_5: |
||||
// subframes 4-5 do not have IOD to match, so we assume they always match
|
||||
issueOfDataMatches = true; |
||||
break; |
||||
default: |
||||
throw new IllegalArgumentException("Invalid subframe provided: " + subframe); |
||||
} |
||||
boolean hasDecodedSubframe = hasDecodedSubframe(subframe); |
||||
return new SubframeCheckResult(hasDecodedSubframe, issueOfDataMatches); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Represents a result while finding a subframe in an intermediate {@link IntermediateEphemeris}. |
||||
*/ |
||||
private static class SubframeCheckResult { |
||||
|
||||
/** |
||||
* The intermediate {@link IntermediateEphemeris} has the requested subframe. |
||||
*/ |
||||
public final boolean hasSubframe; |
||||
|
||||
/** |
||||
* The issue of data, associated with the requested subframe, matches the subframe found in the |
||||
* intermediate state. |
||||
*/ |
||||
public final boolean issueOfDataMatches; |
||||
|
||||
public SubframeCheckResult(boolean hasSubframe, boolean issueOfDataMatches) { |
||||
this.hasSubframe = hasSubframe; |
||||
this.issueOfDataMatches = issueOfDataMatches; |
||||
} |
||||
|
||||
/** |
||||
* @return {@code true} if the requested subframe has been decoded in the intermediate state, |
||||
* {@code false} otherwise. |
||||
*/ |
||||
public boolean isSubframeDecoded() { |
||||
return hasSubframe && issueOfDataMatches; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,315 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.location.lbs.gnss.gps.pseudorange; |
||||
|
||||
import android.util.Pair; |
||||
import com.google.common.base.Preconditions; |
||||
import com.google.common.primitives.Longs; |
||||
import java.util.Calendar; |
||||
import java.util.concurrent.TimeUnit; |
||||
import java.time.ZoneId; |
||||
import java.time.ZonedDateTime; |
||||
import java.time.Instant; |
||||
import java.util.GregorianCalendar; |
||||
|
||||
/** |
||||
* A simple class to represent time unit used by GPS. |
||||
*/ |
||||
public class GpsTime implements Comparable<GpsTime> { |
||||
public static final int MILLIS_IN_SECOND = 1000; |
||||
public static final int SECONDS_IN_MINUTE = 60; |
||||
public static final int MINUTES_IN_HOUR = 60; |
||||
public static final int HOURS_IN_DAY = 24; |
||||
public static final int SECONDS_IN_DAY = |
||||
HOURS_IN_DAY * MINUTES_IN_HOUR * SECONDS_IN_MINUTE; |
||||
public static final int DAYS_IN_WEEK = 7; |
||||
public static final long MILLIS_IN_DAY = TimeUnit.DAYS.toMillis(1); |
||||
public static final long MILLIS_IN_WEEK = TimeUnit.DAYS.toMillis(7); |
||||
public static final long NANOS_IN_WEEK = TimeUnit.DAYS.toNanos(7); |
||||
// GPS epoch is 1980/01/06
|
||||
public static final long GPS_DAYS_SINCE_JAVA_EPOCH = 3657; |
||||
public static final long GPS_UTC_EPOCH_OFFSET_SECONDS = |
||||
TimeUnit.DAYS.toSeconds(GPS_DAYS_SINCE_JAVA_EPOCH); |
||||
public static final long GPS_UTC_EPOCH_OFFSET_NANOS = |
||||
TimeUnit.SECONDS.toNanos(GPS_UTC_EPOCH_OFFSET_SECONDS); |
||||
private static final ZonedDateTime LEAP_SECOND_DATE_1981 = getZonedDateTimeUTC(1981, 7, 1); |
||||
private static final ZonedDateTime LEAP_SECOND_DATE_2012 = getZonedDateTimeUTC(2012, 7, 1); |
||||
private static final ZonedDateTime LEAP_SECOND_DATE_2015 = getZonedDateTimeUTC(2015, 7, 1); |
||||
private static final ZonedDateTime LEAP_SECOND_DATE_2017 = getZonedDateTimeUTC(2017, 7, 1); |
||||
private static final long nanoSecPerSec = TimeUnit.SECONDS.toNanos(7); |
||||
// nanoseconds since GPS epoch (1980/1/6).
|
||||
private long gpsNanos; |
||||
private static ZonedDateTime getZonedDateTimeUTC(int year, int month, int day) { |
||||
return getZonedDateTimeUTC(year, month, day, 0, 0, 0, 0); |
||||
} |
||||
|
||||
private static ZonedDateTime getZonedDateTimeUTC(int year, int month, int day, |
||||
int hour, int minute, int sec, int nanoSec){ |
||||
ZoneId zone = ZoneId.of("UTC"); |
||||
ZonedDateTime zdt = ZonedDateTime.of(year, month, day, hour, minute, sec, nanoSec, zone); |
||||
return zdt; |
||||
} |
||||
|
||||
private static long getMillisFromZonedDateTime(ZonedDateTime zdt) { |
||||
return zdt.toInstant().toEpochMilli(); |
||||
} |
||||
/** |
||||
* Constructor for GpsTime. Input values are all in GPS time. |
||||
* @param year Year |
||||
* @param month Month from 1 to 12 |
||||
* @param day Day from 1 to 31 |
||||
* @param hour Hour from 0 to 23 |
||||
* @param minute Minute from 0 to 59 |
||||
* @param second Second from 0 to 59 |
||||
*/ |
||||
public GpsTime(int year, int month, int day, int hour, int minute, double second) { |
||||
ZonedDateTime utcDateTime = getZonedDateTimeUTC(year, month, day, hour, minute, |
||||
(int) second, (int) ((second * nanoSecPerSec) % nanoSecPerSec)); |
||||
|
||||
|
||||
// Since input time is already specify in GPS time, no need to count leap second here.
|
||||
initGpsNanos(utcDateTime); |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Constructor |
||||
* @param zDateTime is created using GPS time values. |
||||
*/ |
||||
public GpsTime(ZonedDateTime zDateTime) { |
||||
initGpsNanos(zDateTime); |
||||
} |
||||
|
||||
public void initGpsNanos(ZonedDateTime zDateTime){ |
||||
this.gpsNanos = TimeUnit.MILLISECONDS.toNanos(getMillisFromZonedDateTime(zDateTime)) |
||||
- GPS_UTC_EPOCH_OFFSET_NANOS; |
||||
} |
||||
/** |
||||
* Constructor |
||||
* @param gpsNanos nanoseconds since GPS epoch. |
||||
*/ |
||||
public GpsTime(long gpsNanos) { |
||||
this.gpsNanos = gpsNanos; |
||||
} |
||||
|
||||
/** |
||||
* Creates a GPS time using a UTC based date and time. |
||||
* @param zDateTime represents the current time in UTC time, must be after 2009 |
||||
*/ |
||||
public static GpsTime fromUtc(ZonedDateTime zDateTime) { |
||||
return new GpsTime(TimeUnit.MILLISECONDS.toNanos(getMillisFromZonedDateTime(zDateTime)) |
||||
+ TimeUnit.SECONDS.toNanos( |
||||
GpsTime.getLeapSecond(zDateTime) - GPS_UTC_EPOCH_OFFSET_SECONDS)); |
||||
} |
||||
|
||||
/** |
||||
* Creates a GPS time based upon the current time. |
||||
*/ |
||||
public static GpsTime now() { |
||||
ZoneId zone = ZoneId.of("UTC"); |
||||
ZonedDateTime current = ZonedDateTime.now(zone); |
||||
return fromUtc(current); |
||||
} |
||||
|
||||
/** |
||||
* Creates a GPS time using absolute GPS week number, and the time of week. |
||||
* @param gpsWeek |
||||
* @param towSec GPS time of week in second |
||||
* @return actual time in GpsTime. |
||||
*/ |
||||
public static GpsTime fromWeekTow(int gpsWeek, int towSec) { |
||||
long nanos = gpsWeek * NANOS_IN_WEEK + TimeUnit.SECONDS.toNanos(towSec); |
||||
return new GpsTime(nanos); |
||||
} |
||||
|
||||
/** |
||||
* Creates a GPS time using YUMA GPS week number (0..1023), and the time of week. |
||||
* @param yumaWeek (0..1023) |
||||
* @param towSec GPS time of week in second |
||||
* @return actual time in GpsTime. |
||||
*/ |
||||
public static GpsTime fromYumaWeekTow(int yumaWeek, int towSec) { |
||||
Preconditions.checkArgument(yumaWeek >= 0); |
||||
Preconditions.checkArgument(yumaWeek < 1024); |
||||
|
||||
// Estimate the multiplier of current week.
|
||||
ZoneId zone = ZoneId.of("UTC"); |
||||
ZonedDateTime current = ZonedDateTime.now(zone); |
||||
GpsTime refTime = new GpsTime(current); |
||||
Pair<Integer, Integer> refWeekSec = refTime.getGpsWeekSecond(); |
||||
int weekMultiplier = refWeekSec.first / 1024; |
||||
|
||||
int gpsWeek = weekMultiplier * 1024 + yumaWeek; |
||||
return fromWeekTow(gpsWeek, towSec); |
||||
} |
||||
|
||||
public static GpsTime fromTimeSinceGpsEpoch(long gpsSec) { |
||||
return new GpsTime(TimeUnit.SECONDS.toNanos(gpsSec)); |
||||
} |
||||
|
||||
/** |
||||
* Computes leap seconds. Only accurate after 2009. |
||||
* @param time |
||||
* @return number of leap seconds since GPS epoch. |
||||
*/ |
||||
public static int getLeapSecond(ZonedDateTime time) { |
||||
if (LEAP_SECOND_DATE_2017.compareTo(time) <= 0) { |
||||
return 18; |
||||
} else if (LEAP_SECOND_DATE_2015.compareTo(time) <= 0) { |
||||
return 17; |
||||
} else if (LEAP_SECOND_DATE_2012.compareTo(time) <= 0) { |
||||
return 16; |
||||
} else if (LEAP_SECOND_DATE_1981.compareTo(time) <= 0) { |
||||
// Only correct between 2012/7/1 to 2008/12/31
|
||||
return 15; |
||||
} else { |
||||
return 0; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Computes GPS weekly epoch of the reference time. |
||||
* <p>GPS weekly epoch are defined as of every Sunday 00:00:000 (mor |
||||
* @param refTime reference time |
||||
* @return nanoseconds since GPS epoch, for the week epoch. |
||||
*/ |
||||
public static Long getGpsWeekEpochNano(GpsTime refTime) { |
||||
Pair<Integer, Integer> weekSecond = refTime.getGpsWeekSecond(); |
||||
return weekSecond.first * NANOS_IN_WEEK; |
||||
} |
||||
|
||||
/** |
||||
* @return week count since GPS epoch, and second count since the beginning of |
||||
* that week. |
||||
*/ |
||||
public Pair<Integer, Integer> getGpsWeekSecond() { |
||||
// JAVA/UNIX epoch: January 1, 1970 in msec
|
||||
// GPS epoch: January 6, 1980 in second
|
||||
int week = (int) (gpsNanos / NANOS_IN_WEEK); |
||||
int second = (int) TimeUnit.NANOSECONDS.toSeconds(gpsNanos % NANOS_IN_WEEK); |
||||
return Pair.create(week, second); |
||||
} |
||||
|
||||
/** |
||||
* @return week count since GPS epoch, and second count in 0.08 sec |
||||
* resolution, 23-bit presentation (required by RRLP.)" |
||||
*/ |
||||
public Pair<Integer, Integer> getGpsWeekTow23b() { |
||||
// UNIX epoch: January 1, 1970 in msec
|
||||
// GPS epoch: January 6, 1980 in second
|
||||
int week = (int) (gpsNanos / NANOS_IN_WEEK); |
||||
// 80 millis is 0.08 second.
|
||||
int tow23b = (int) TimeUnit.NANOSECONDS.toMillis(gpsNanos % NANOS_IN_WEEK) / 80; |
||||
return Pair.create(week, tow23b); |
||||
} |
||||
|
||||
/** |
||||
* @return Day of year in GPS time (GMT time) |
||||
*/ |
||||
public static int getCurrentDayOfYear() { |
||||
ZoneId zone = ZoneId.of("UTC"); |
||||
ZonedDateTime current = ZonedDateTime.now(zone); |
||||
// Since current is derived from UTC time, we need to add leap second here.
|
||||
long gpsTimeMillis = getMillisFromZonedDateTime(current) |
||||
+ TimeUnit.SECONDS.toMillis(getLeapSecond(current)); |
||||
ZonedDateTime gpsCurrent = ZonedDateTime.ofInstant(Instant.ofEpochMilli(gpsTimeMillis), ZoneId.of("UTC")); |
||||
return gpsCurrent.getDayOfYear(); |
||||
} |
||||
|
||||
/** |
||||
* @return milliseconds since JAVA/UNIX epoch. |
||||
*/ |
||||
public final long getMillisSinceJavaEpoch() { |
||||
return TimeUnit.NANOSECONDS.toMillis(gpsNanos + GPS_UTC_EPOCH_OFFSET_NANOS); |
||||
} |
||||
|
||||
/** |
||||
* @return milliseconds since GPS epoch. |
||||
*/ |
||||
public final long getMillisSinceGpsEpoch() { |
||||
return TimeUnit.NANOSECONDS.toMillis(gpsNanos); |
||||
} |
||||
|
||||
/** |
||||
* @return microseconds since GPS epoch. |
||||
*/ |
||||
public final long getMicrosSinceGpsEpoch() { |
||||
return TimeUnit.NANOSECONDS.toMicros(gpsNanos); |
||||
} |
||||
|
||||
/** |
||||
* @return nanoseconds since GPS epoch. |
||||
*/ |
||||
public final long getNanosSinceGpsEpoch() { |
||||
return gpsNanos; |
||||
} |
||||
|
||||
/** |
||||
* @return the GPS time in Calendar. |
||||
*/ |
||||
public Calendar getTimeInCalendar() { |
||||
return GregorianCalendar.from(getGpsDateTime()); |
||||
} |
||||
|
||||
/** |
||||
* @return a ZonedDateTime with leap seconds considered. |
||||
*/ |
||||
public ZonedDateTime getUtcDateTime() { |
||||
ZonedDateTime gpsDateTime = getGpsDateTime(); |
||||
long gpsMillis = getMillisFromZonedDateTime(gpsDateTime) |
||||
- TimeUnit.SECONDS.toMillis(getLeapSecond(gpsDateTime)); |
||||
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(gpsMillis), ZoneId.of("UTC")); |
||||
|
||||
} |
||||
|
||||
/** |
||||
* @return a ZonedDateTime based on the pure GPS time (without considering leap second). |
||||
*/ |
||||
public ZonedDateTime getGpsDateTime() { |
||||
long gpsMillis = TimeUnit.NANOSECONDS.toMillis(gpsNanos + GPS_UTC_EPOCH_OFFSET_NANOS); |
||||
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(gpsMillis), ZoneId.of("UTC")); |
||||
} |
||||
|
||||
/** |
||||
* Compares two {@code GpsTime} objects temporally. |
||||
* |
||||
* @param other the {@code GpsTime} to be compared. |
||||
* @return the value {@code 0} if this {@code GpsTime} is simultaneous with |
||||
* the argument {@code GpsTime}; a value less than {@code 0} if this |
||||
* {@code GpsTime} occurs before the argument {@code GpsTime}; and |
||||
* a value greater than {@code 0} if this {@code GpsTime} occurs |
||||
* after the argument {@code GpsTime} (signed comparison). |
||||
*/ |
||||
@Override |
||||
public int compareTo(GpsTime other) { |
||||
return Long.compare(this.getNanosSinceGpsEpoch(), other.getNanosSinceGpsEpoch()); |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object other) { |
||||
if (!(other instanceof GpsTime)) { |
||||
return false; |
||||
} |
||||
GpsTime time = (GpsTime) other; |
||||
return getNanosSinceGpsEpoch() == time.getNanosSinceGpsEpoch(); |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
return Longs.hashCode(getNanosSinceGpsEpoch()); |
||||
} |
||||
} |
@ -0,0 +1,139 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.location.lbs.gnss.gps.pseudorange; |
||||
|
||||
import com.google.location.lbs.gnss.gps.pseudorange.Ecef2LlaConverter.GeodeticLlaValues; |
||||
import com.google.location.lbs.gnss.gps.pseudorange.EcefToTopocentricConverter.TopocentricAEDValues; |
||||
|
||||
/** |
||||
* Calculates the Ionospheric correction of the pseudorange given the {@code userPosition}, |
||||
* {@code satellitePosition}, {@code gpsTimeSeconds} and the ionospheric parameters sent by the |
||||
* satellite {@code alpha} and {@code beta} |
||||
* |
||||
* <p>Source: http://www.navipedia.net/index.php/Klobuchar_Ionospheric_Model and
|
||||
* http://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=4104345 and
|
||||
* http://www.ion.org/museum/files/ACF2A4.pdf
|
||||
*/ |
||||
public class IonosphericModel { |
||||
/** Center frequency of the L1 band in Hz. */ |
||||
public static final double L1_FREQ_HZ = 10.23 * 1e6 * 154; |
||||
/** Center frequency of the L2 band in Hz. */ |
||||
public static final double L2_FREQ_HZ = 10.23 * 1e6 * 120; |
||||
/** Center frequency of the L5 band in Hz. */ |
||||
public static final double L5_FREQ_HZ = 10.23 * 1e6 * 115; |
||||
|
||||
private static final double SECONDS_PER_DAY = 86400.0; |
||||
private static final double PERIOD_OF_DELAY_TRHESHOLD_SECONDS = 72000.0; |
||||
private static final double IPP_LATITUDE_THRESHOLD_SEMI_CIRCLE = 0.416; |
||||
private static final double DC_TERM = 5.0e-9; |
||||
private static final double NORTH_GEOMAGNETIC_POLE_LONGITUDE_RADIANS = 5.08; |
||||
private static final double GEOMETRIC_LATITUDE_CONSTANT = 0.064; |
||||
private static final int DELAY_PHASE_TIME_CONSTANT_SECONDS = 50400; |
||||
private static final int IONO_0_IDX = 0; |
||||
private static final int IONO_1_IDX = 1; |
||||
private static final int IONO_2_IDX = 2; |
||||
private static final int IONO_3_IDX = 3; |
||||
|
||||
/** |
||||
* Calculates the Ionospheric correction of the pseudorane in seconds using the Klobuchar |
||||
* Ionospheric model. |
||||
*/ |
||||
public static double ionoKloboucharCorrectionSeconds( |
||||
double[] userPositionECEFMeters, |
||||
double[] satellitePositionECEFMeters, |
||||
double gpsTOWSeconds, |
||||
double[] alpha, |
||||
double[] beta, |
||||
double frequencyHz) { |
||||
|
||||
TopocentricAEDValues elevationAndAzimuthRadians = EcefToTopocentricConverter |
||||
.calculateElAzDistBetween2Points(userPositionECEFMeters, satellitePositionECEFMeters); |
||||
double elevationSemiCircle = elevationAndAzimuthRadians.elevationRadians / Math.PI; |
||||
double azimuthSemiCircle = elevationAndAzimuthRadians.azimuthRadians / Math.PI; |
||||
GeodeticLlaValues latLngAlt = Ecef2LlaConverter.convertECEFToLLACloseForm( |
||||
userPositionECEFMeters[0], userPositionECEFMeters[1], userPositionECEFMeters[2]); |
||||
double latitudeUSemiCircle = latLngAlt.latitudeRadians / Math.PI; |
||||
double longitudeUSemiCircle = latLngAlt.longitudeRadians / Math.PI; |
||||
|
||||
// earth's centered angle (semi-circles)
|
||||
double earthCentredAngleSemiCirle = 0.0137 / (elevationSemiCircle + 0.11) - 0.022; |
||||
|
||||
// latitude of the Ionospheric Pierce Point (IPP) (semi-circles)
|
||||
double latitudeISemiCircle = |
||||
latitudeUSemiCircle + earthCentredAngleSemiCirle * Math.cos(azimuthSemiCircle * Math.PI); |
||||
|
||||
if (latitudeISemiCircle > IPP_LATITUDE_THRESHOLD_SEMI_CIRCLE) { |
||||
latitudeISemiCircle = IPP_LATITUDE_THRESHOLD_SEMI_CIRCLE; |
||||
} else if (latitudeISemiCircle < -IPP_LATITUDE_THRESHOLD_SEMI_CIRCLE) { |
||||
latitudeISemiCircle = -IPP_LATITUDE_THRESHOLD_SEMI_CIRCLE; |
||||
} |
||||
|
||||
// geodetic longitude of the Ionospheric Pierce Point (IPP) (semi-circles)
|
||||
double longitudeISemiCircle = longitudeUSemiCircle + earthCentredAngleSemiCirle |
||||
* Math.sin(azimuthSemiCircle * Math.PI) / Math.cos(latitudeISemiCircle * Math.PI); |
||||
|
||||
// geomagnetic latitude of the Ionospheric Pierce Point (IPP) (semi-circles)
|
||||
double geomLatIPPSemiCircle = latitudeISemiCircle + GEOMETRIC_LATITUDE_CONSTANT |
||||
* Math.cos(longitudeISemiCircle * Math.PI - NORTH_GEOMAGNETIC_POLE_LONGITUDE_RADIANS); |
||||
|
||||
// local time (sec) at the Ionospheric Pierce Point (IPP)
|
||||
double localTimeSeconds = SECONDS_PER_DAY / 2.0 * longitudeISemiCircle + gpsTOWSeconds; |
||||
localTimeSeconds %= SECONDS_PER_DAY; |
||||
if (localTimeSeconds < 0) { |
||||
localTimeSeconds += SECONDS_PER_DAY; |
||||
} |
||||
|
||||
// amplitude of the ionospheric delay (seconds)
|
||||
double amplitudeOfDelaySeconds = alpha[IONO_0_IDX] + alpha[IONO_1_IDX] * geomLatIPPSemiCircle |
||||
+ alpha[IONO_2_IDX] * geomLatIPPSemiCircle * geomLatIPPSemiCircle + alpha[IONO_3_IDX] |
||||
* geomLatIPPSemiCircle * geomLatIPPSemiCircle * geomLatIPPSemiCircle; |
||||
if (amplitudeOfDelaySeconds < 0) { |
||||
amplitudeOfDelaySeconds = 0; |
||||
} |
||||
|
||||
// period of ionospheric delay
|
||||
double periodOfDelaySeconds = beta[IONO_0_IDX] + beta[IONO_1_IDX] * geomLatIPPSemiCircle |
||||
+ beta[IONO_2_IDX] * geomLatIPPSemiCircle * geomLatIPPSemiCircle + beta[IONO_3_IDX] |
||||
* geomLatIPPSemiCircle * geomLatIPPSemiCircle * geomLatIPPSemiCircle; |
||||
if (periodOfDelaySeconds < PERIOD_OF_DELAY_TRHESHOLD_SECONDS) { |
||||
periodOfDelaySeconds = PERIOD_OF_DELAY_TRHESHOLD_SECONDS; |
||||
} |
||||
|
||||
// phase of ionospheric delay
|
||||
double phaseOfDelayRadians = |
||||
2 * Math.PI * (localTimeSeconds - DELAY_PHASE_TIME_CONSTANT_SECONDS) / periodOfDelaySeconds; |
||||
|
||||
// slant factor
|
||||
double slantFactor = 1.0 + 16.0 * Math.pow(0.53 - elevationSemiCircle, 3); |
||||
|
||||
// ionospheric time delay (seconds)
|
||||
double ionoDelaySeconds; |
||||
|
||||
if (Math.abs(phaseOfDelayRadians) >= Math.PI / 2.0) { |
||||
ionoDelaySeconds = DC_TERM * slantFactor; |
||||
} else { |
||||
ionoDelaySeconds = (DC_TERM |
||||
+ (1 - Math.pow(phaseOfDelayRadians, 2) / 2.0 + Math.pow(phaseOfDelayRadians, 4) / 24.0) |
||||
* amplitudeOfDelaySeconds) * slantFactor; |
||||
} |
||||
|
||||
// apply factor for frequency bands other than L1
|
||||
ionoDelaySeconds *= (L1_FREQ_HZ * L1_FREQ_HZ) / (frequencyHz * frequencyHz); |
||||
|
||||
return ionoDelaySeconds; |
||||
} |
||||
} |
@ -0,0 +1,57 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.location.lbs.gnss.gps.pseudorange; |
||||
|
||||
import com.google.location.lbs.gnss.gps.pseudorange.Ecef2LlaConverter.GeodeticLlaValues; |
||||
|
||||
/** |
||||
* A tool to convert geodetic latitude, longitude and altitude above planetary ellipsoid to |
||||
* Earth-centered Earth-fixed (ECEF) Cartesian coordinates |
||||
* |
||||
* <p>Source: https://www.mathworks.com/help/aeroblks/llatoecefposition.html
|
||||
*/ |
||||
public class Lla2EcefConverter { |
||||
private static final double ECCENTRICITY = 8.1819190842622e-2; |
||||
private static final double EARTH_SEMI_MAJOR_AXIS_METERS = 6378137.0; |
||||
|
||||
/** |
||||
* Converts LLA (latitude,longitude, and altitude) coordinates to ECEF |
||||
* (Earth-Centered Earth-Fixed) Cartesian coordinates |
||||
* |
||||
* <p>Inputs is GeodeticLlaValues class {@link GeodeticLlaValues} containing geodetic latitude |
||||
* (radians), geodetic longitude (radians), height above WGS84 ellipsoid (m) |
||||
* |
||||
* <p>Output is cartesian coordinates x,y,z in meters |
||||
*/ |
||||
public static double[] convertFromLlaToEcefMeters(GeodeticLlaValues llaValues) { |
||||
double cosLatitude = Math.cos(llaValues.latitudeRadians); |
||||
double cosLongitude = Math.cos(llaValues.longitudeRadians); |
||||
double sinLatitude = Math.sin(llaValues.latitudeRadians); |
||||
double sinLongitude = Math.sin(llaValues.longitudeRadians); |
||||
|
||||
double r0 = |
||||
EARTH_SEMI_MAJOR_AXIS_METERS |
||||
/ Math.sqrt(1.0 - Math.pow(ECCENTRICITY, 2) * sinLatitude * sinLatitude); |
||||
|
||||
double[] positionEcefMeters = new double[3]; |
||||
positionEcefMeters[0] = (llaValues.altitudeMeters + r0) * cosLatitude * cosLongitude; |
||||
positionEcefMeters[1] = (llaValues.altitudeMeters + r0) * cosLatitude * sinLongitude; |
||||
positionEcefMeters[2] = |
||||
(llaValues.altitudeMeters + r0 * (1.0 - Math.pow(ECCENTRICITY, 2))) * sinLatitude; |
||||
return positionEcefMeters; |
||||
} |
||||
} |
@ -0,0 +1,36 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.location.lbs.gnss.gps.pseudorange; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* An implementation of {@link PseudorangeSmoother} that performs no smoothing. |
||||
* |
||||
* <p> A new list of {@link GpsMeasurementWithRangeAndUncertainty} instances is filled with a copy |
||||
* of the input list. |
||||
*/ |
||||
class PseudorangeNoSmoothingSmoother implements PseudorangeSmoother { |
||||
|
||||
@Override |
||||
public List<GpsMeasurementWithRangeAndUncertainty> updatePseudorangeSmoothingResult( |
||||
List<GpsMeasurementWithRangeAndUncertainty> usefulSatellitesToGPSReceiverMeasurements) { |
||||
return Collections.unmodifiableList(usefulSatellitesToGPSReceiverMeasurements); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,500 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.location.lbs.gnss.gps.pseudorange; |
||||
|
||||
import android.location.GnssClock; |
||||
import android.location.GnssMeasurement; |
||||
import android.location.GnssMeasurementsEvent; |
||||
import android.location.GnssNavigationMessage; |
||||
import android.location.GnssStatus; |
||||
import android.util.Log; |
||||
import com.google.location.lbs.gnss.gps.pseudorange.Ecef2EnuConverter.EnuValues; |
||||
import com.google.location.lbs.gnss.gps.pseudorange.Ecef2LlaConverter.GeodeticLlaValues; |
||||
import android.location.cts.nano.Ephemeris.GpsEphemerisProto; |
||||
import android.location.cts.nano.Ephemeris.GpsNavMessageProto; |
||||
import android.location.cts.suplClient.SuplRrlpController; |
||||
import java.io.BufferedReader; |
||||
import java.io.IOException; |
||||
import java.net.UnknownHostException; |
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.Calendar; |
||||
import java.util.List; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
/** |
||||
* Helper class for calculating Gps position and velocity solution using weighted least squares |
||||
* where the raw Gps measurements are parsed as a {@link BufferedReader} with the option to apply |
||||
* doppler smoothing, carrier phase smoothing or no smoothing. |
||||
* |
||||
*/ |
||||
public class PseudorangePositionVelocityFromRealTimeEvents { |
||||
|
||||
private static final String TAG = "PseudorangePositionVelocityFromRealTimeEvents"; |
||||
private static final double SECONDS_PER_NANO = 1.0e-9; |
||||
private static final int TOW_DECODED_MEASUREMENT_STATE_BIT = 3; |
||||
/** Average signal travel time from GPS satellite and earth */ |
||||
private static final int VALID_ACCUMULATED_DELTA_RANGE_STATE = 1; |
||||
private static final int MINIMUM_NUMBER_OF_USEFUL_SATELLITES = 4; |
||||
private static final int C_TO_N0_THRESHOLD_DB_HZ = 18; |
||||
|
||||
private static final String SUPL_SERVER_NAME = "supl.google.com"; |
||||
private static final int SUPL_SERVER_PORT = 7276; |
||||
|
||||
private GpsNavMessageProto mHardwareGpsNavMessageProto = null; |
||||
|
||||
// navigation message parser
|
||||
private GpsNavigationMessageStore mGpsNavigationMessageStore = new GpsNavigationMessageStore(); |
||||
private double[] mPositionSolutionLatLngDeg = GpsMathOperations.createAndFillArray(3, Double.NaN); |
||||
private double[] mVelocitySolutionEnuMps = GpsMathOperations.createAndFillArray(3, Double.NaN); |
||||
private final double[] mPositionVelocityUncertaintyEnu |
||||
= GpsMathOperations.createAndFillArray(6, Double.NaN); |
||||
private double[] mPseudorangeResidualsMeters = |
||||
GpsMathOperations.createAndFillArray( |
||||
GpsNavigationMessageStore.MAX_NUMBER_OF_SATELLITES, Double.NaN |
||||
); |
||||
private boolean mFirstUsefulMeasurementSet = true; |
||||
private int[] mReferenceLocation = null; |
||||
private long mLastReceivedSuplMessageTimeMillis = 0; |
||||
private long mDeltaTimeMillisToMakeSuplRequest = TimeUnit.MINUTES.toMillis(30); |
||||
private boolean mFirstSuplRequestNeeded = true; |
||||
private GpsNavMessageProto mGpsNavMessageProtoUsed = null; |
||||
|
||||
// Only the interface of pseudorange smoother is provided. Please implement customized smoother.
|
||||
PseudorangeSmoother mPseudorangeSmoother = new PseudorangeNoSmoothingSmoother(); |
||||
private final UserPositionVelocityWeightedLeastSquare mUserPositionVelocityLeastSquareCalculator = |
||||
new UserPositionVelocityWeightedLeastSquare(mPseudorangeSmoother); |
||||
private GpsMeasurement[] mUsefulSatellitesToReceiverMeasurements = |
||||
new GpsMeasurement[GpsNavigationMessageStore.MAX_NUMBER_OF_SATELLITES]; |
||||
private Long[] mUsefulSatellitesToTowNs = |
||||
new Long[GpsNavigationMessageStore.MAX_NUMBER_OF_SATELLITES]; |
||||
private long mLargestTowNs = Long.MIN_VALUE; |
||||
private double mArrivalTimeSinceGPSWeekNs = 0.0; |
||||
private int mDayOfYear1To366 = 0; |
||||
private int mGpsWeekNumber = 0; |
||||
private long mArrivalTimeSinceGpsEpochNs = 0; |
||||
|
||||
/** |
||||
* Computes Weighted least square position and velocity solutions from a received {@link |
||||
* GnssMeasurementsEvent} and store the result in {@link |
||||
* PseudorangePositionVelocityFromRealTimeEvents#mPositionSolutionLatLngDeg} and {@link |
||||
* PseudorangePositionVelocityFromRealTimeEvents#mVelocitySolutionEnuMps} |
||||
*/ |
||||
public void computePositionVelocitySolutionsFromRawMeas(GnssMeasurementsEvent event) |
||||
throws Exception { |
||||
if (mReferenceLocation == null) { |
||||
// If no reference location is received, we can not get navigation message from SUPL and hence
|
||||
// we will not try to compute location.
|
||||
Log.d(TAG, " No reference Location ..... no position is calculated"); |
||||
return; |
||||
} |
||||
for (int i = 0; i < GpsNavigationMessageStore.MAX_NUMBER_OF_SATELLITES; i++) { |
||||
mUsefulSatellitesToReceiverMeasurements[i] = null; |
||||
mUsefulSatellitesToTowNs[i] = null; |
||||
} |
||||
|
||||
GnssClock gnssClock = event.getClock(); |
||||
mArrivalTimeSinceGpsEpochNs = gnssClock.getTimeNanos() - gnssClock.getFullBiasNanos(); |
||||
|
||||
for (GnssMeasurement measurement : event.getMeasurements()) { |
||||
// ignore any measurement if it is not from GPS constellation
|
||||
if (measurement.getConstellationType() != GnssStatus.CONSTELLATION_GPS) { |
||||
continue; |
||||
} |
||||
// ignore raw data if time is zero, if signal to noise ratio is below threshold or if
|
||||
// TOW is not yet decoded
|
||||
if (measurement.getCn0DbHz() >= C_TO_N0_THRESHOLD_DB_HZ |
||||
&& (measurement.getState() & (1L << TOW_DECODED_MEASUREMENT_STATE_BIT)) != 0) { |
||||
|
||||
// calculate day of year and Gps week number needed for the least square
|
||||
GpsTime gpsTime = new GpsTime(mArrivalTimeSinceGpsEpochNs); |
||||
// Gps weekly epoch in Nanoseconds: defined as of every Sunday night at 00:00:000
|
||||
long gpsWeekEpochNs = GpsTime.getGpsWeekEpochNano(gpsTime); |
||||
mArrivalTimeSinceGPSWeekNs = mArrivalTimeSinceGpsEpochNs - gpsWeekEpochNs; |
||||
mGpsWeekNumber = gpsTime.getGpsWeekSecond().first; |
||||
// calculate day of the year between 1 and 366
|
||||
Calendar cal = gpsTime.getTimeInCalendar(); |
||||
mDayOfYear1To366 = cal.get(Calendar.DAY_OF_YEAR); |
||||
|
||||
long receivedGPSTowNs = measurement.getReceivedSvTimeNanos(); |
||||
if (receivedGPSTowNs > mLargestTowNs) { |
||||
mLargestTowNs = receivedGPSTowNs; |
||||
} |
||||
mUsefulSatellitesToTowNs[measurement.getSvid() - 1] = receivedGPSTowNs; |
||||
GpsMeasurement gpsReceiverMeasurement = |
||||
new GpsMeasurement( |
||||
(long) mArrivalTimeSinceGPSWeekNs, |
||||
measurement.getAccumulatedDeltaRangeMeters(), |
||||
measurement.getAccumulatedDeltaRangeState() == VALID_ACCUMULATED_DELTA_RANGE_STATE, |
||||
measurement.getPseudorangeRateMetersPerSecond(), |
||||
measurement.getCn0DbHz(), |
||||
measurement.getAccumulatedDeltaRangeUncertaintyMeters(), |
||||
measurement.getPseudorangeRateUncertaintyMetersPerSecond()); |
||||
mUsefulSatellitesToReceiverMeasurements[measurement.getSvid() - 1] = gpsReceiverMeasurement; |
||||
} |
||||
} |
||||
|
||||
// check if we should continue using the navigation message from the SUPL server, or use the
|
||||
// navigation message from the device if we fully received it
|
||||
boolean useNavMessageFromSupl = |
||||
continueUsingNavMessageFromSupl( |
||||
mUsefulSatellitesToReceiverMeasurements, mHardwareGpsNavMessageProto); |
||||
if (useNavMessageFromSupl) { |
||||
Log.d(TAG, "Using navigation message from SUPL server"); |
||||
|
||||
if (mFirstSuplRequestNeeded |
||||
|| (System.currentTimeMillis() - mLastReceivedSuplMessageTimeMillis) |
||||
> mDeltaTimeMillisToMakeSuplRequest) { |
||||
// The following line is blocking call for SUPL connection and back. But it is fast enough
|
||||
mGpsNavMessageProtoUsed = getSuplNavMessage(mReferenceLocation[0], mReferenceLocation[1]); |
||||
if (!isEmptyNavMessage(mGpsNavMessageProtoUsed)) { |
||||
mFirstSuplRequestNeeded = false; |
||||
mLastReceivedSuplMessageTimeMillis = System.currentTimeMillis(); |
||||
} else { |
||||
return; |
||||
} |
||||
} |
||||
|
||||
} else { |
||||
Log.d(TAG, "Using navigation message from the GPS receiver"); |
||||
mGpsNavMessageProtoUsed = mHardwareGpsNavMessageProto; |
||||
} |
||||
|
||||
// some times the SUPL server returns less satellites than the visible ones, so remove those
|
||||
// visible satellites that are not returned by SUPL
|
||||
for (int i = 0; i < GpsNavigationMessageStore.MAX_NUMBER_OF_SATELLITES; i++) { |
||||
if (mUsefulSatellitesToReceiverMeasurements[i] != null |
||||
&& !navMessageProtoContainsSvid(mGpsNavMessageProtoUsed, i + 1)) { |
||||
mUsefulSatellitesToReceiverMeasurements[i] = null; |
||||
mUsefulSatellitesToTowNs[i] = null; |
||||
} |
||||
} |
||||
|
||||
// calculate the number of useful satellites
|
||||
int numberOfUsefulSatellites = 0; |
||||
for (GpsMeasurement element : mUsefulSatellitesToReceiverMeasurements) { |
||||
if (element != null) { |
||||
numberOfUsefulSatellites++; |
||||
} |
||||
} |
||||
if (numberOfUsefulSatellites >= MINIMUM_NUMBER_OF_USEFUL_SATELLITES) { |
||||
// ignore first set of > 4 satellites as they often result in erroneous position
|
||||
if (!mFirstUsefulMeasurementSet) { |
||||
// start with last known position and velocity of zero. Following the structure:
|
||||
// [X position, Y position, Z position, clock bias,
|
||||
// X Velocity, Y Velocity, Z Velocity, clock bias rate]
|
||||
double[] positionVeloctySolutionEcef = GpsMathOperations.createAndFillArray(8, 0); |
||||
double[] positionVelocityUncertaintyEnu = GpsMathOperations.createAndFillArray(6, 0); |
||||
double[] pseudorangeResidualMeters |
||||
= GpsMathOperations.createAndFillArray( |
||||
GpsNavigationMessageStore.MAX_NUMBER_OF_SATELLITES, Double.NaN |
||||
); |
||||
performPositionVelocityComputationEcef( |
||||
mUserPositionVelocityLeastSquareCalculator, |
||||
mUsefulSatellitesToReceiverMeasurements, |
||||
mUsefulSatellitesToTowNs, |
||||
mLargestTowNs, |
||||
mArrivalTimeSinceGPSWeekNs, |
||||
mDayOfYear1To366, |
||||
mGpsWeekNumber, |
||||
positionVeloctySolutionEcef, |
||||
positionVelocityUncertaintyEnu, |
||||
pseudorangeResidualMeters); |
||||
// convert the position solution from ECEF to latitude, longitude and altitude
|
||||
GeodeticLlaValues latLngAlt = |
||||
Ecef2LlaConverter.convertECEFToLLACloseForm( |
||||
positionVeloctySolutionEcef[0], |
||||
positionVeloctySolutionEcef[1], |
||||
positionVeloctySolutionEcef[2]); |
||||
mPositionSolutionLatLngDeg[0] = Math.toDegrees(latLngAlt.latitudeRadians); |
||||
mPositionSolutionLatLngDeg[1] = Math.toDegrees(latLngAlt.longitudeRadians); |
||||
mPositionSolutionLatLngDeg[2] = latLngAlt.altitudeMeters; |
||||
mPositionVelocityUncertaintyEnu[0] = positionVelocityUncertaintyEnu[0]; |
||||
mPositionVelocityUncertaintyEnu[1] = positionVelocityUncertaintyEnu[1]; |
||||
mPositionVelocityUncertaintyEnu[2] = positionVelocityUncertaintyEnu[2]; |
||||
System.arraycopy( |
||||
pseudorangeResidualMeters, |
||||
0 /*source starting pos*/, |
||||
mPseudorangeResidualsMeters, |
||||
0 /*destination starting pos*/, |
||||
GpsNavigationMessageStore.MAX_NUMBER_OF_SATELLITES /*length of elements*/ |
||||
); |
||||
Log.d(TAG, |
||||
"Position Uncertainty ENU Meters :" |
||||
+ mPositionVelocityUncertaintyEnu[0] |
||||
+ " " |
||||
+ mPositionVelocityUncertaintyEnu[1] |
||||
+ " " |
||||
+ mPositionVelocityUncertaintyEnu[2]); |
||||
Log.d( |
||||
TAG, |
||||
"Latitude, Longitude, Altitude: " |
||||
+ mPositionSolutionLatLngDeg[0] |
||||
+ " " |
||||
+ mPositionSolutionLatLngDeg[1] |
||||
+ " " |
||||
+ mPositionSolutionLatLngDeg[2]); |
||||
EnuValues velocityEnu = Ecef2EnuConverter.convertEcefToEnu( |
||||
positionVeloctySolutionEcef[4], |
||||
positionVeloctySolutionEcef[5], |
||||
positionVeloctySolutionEcef[6], |
||||
latLngAlt.latitudeRadians, |
||||
latLngAlt.longitudeRadians |
||||
); |
||||
|
||||
mVelocitySolutionEnuMps[0] = velocityEnu.enuEast; |
||||
mVelocitySolutionEnuMps[1] = velocityEnu.enuNorth; |
||||
mVelocitySolutionEnuMps[2] = velocityEnu.enuUP; |
||||
Log.d( |
||||
TAG, |
||||
"Velocity ENU Mps: " |
||||
+ mVelocitySolutionEnuMps[0] |
||||
+ " " |
||||
+ mVelocitySolutionEnuMps[1] |
||||
+ " " |
||||
+ mVelocitySolutionEnuMps[2]); |
||||
mPositionVelocityUncertaintyEnu[3] = positionVelocityUncertaintyEnu[3]; |
||||
mPositionVelocityUncertaintyEnu[4] = positionVelocityUncertaintyEnu[4]; |
||||
mPositionVelocityUncertaintyEnu[5] = positionVelocityUncertaintyEnu[5]; |
||||
Log.d(TAG, |
||||
"Velocity Uncertainty ENU Mps :" |
||||
+ mPositionVelocityUncertaintyEnu[3] |
||||
+ " " |
||||
+ mPositionVelocityUncertaintyEnu[4] |
||||
+ " " |
||||
+ mPositionVelocityUncertaintyEnu[5]); |
||||
} |
||||
mFirstUsefulMeasurementSet = false; |
||||
} else { |
||||
Log.d( |
||||
TAG, |
||||
"Less than four satellites with SNR above threshold visible ... " |
||||
+ "no position is calculated!"); |
||||
|
||||
mPositionSolutionLatLngDeg = GpsMathOperations.createAndFillArray(3, Double.NaN); |
||||
mVelocitySolutionEnuMps = GpsMathOperations.createAndFillArray(3, Double.NaN); |
||||
mPseudorangeResidualsMeters = |
||||
GpsMathOperations.createAndFillArray( |
||||
GpsNavigationMessageStore.MAX_NUMBER_OF_SATELLITES, Double.NaN |
||||
); |
||||
} |
||||
} |
||||
|
||||
private boolean isEmptyNavMessage(GpsNavMessageProto navMessageProto) { |
||||
if(navMessageProto.iono == null)return true; |
||||
if(navMessageProto.ephemerids.length ==0)return true; |
||||
return false; |
||||
} |
||||
|
||||
private boolean navMessageProtoContainsSvid(GpsNavMessageProto navMessageProto, int svid) { |
||||
List<GpsEphemerisProto> ephemeridesList = |
||||
new ArrayList<GpsEphemerisProto>(Arrays.asList(navMessageProto.ephemerids)); |
||||
for (GpsEphemerisProto ephProtoFromList : ephemeridesList) { |
||||
if (ephProtoFromList.prn == svid) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* Calculates ECEF least square position and velocity solutions from an array of {@link |
||||
* GpsMeasurement} in meters and meters per second and store the result in {@code |
||||
* positionVelocitySolutionEcef} |
||||
*/ |
||||
private void performPositionVelocityComputationEcef( |
||||
UserPositionVelocityWeightedLeastSquare userPositionVelocityLeastSquare, |
||||
GpsMeasurement[] usefulSatellitesToReceiverMeasurements, |
||||
Long[] usefulSatellitesToTOWNs, |
||||
long largestTowNs, |
||||
double arrivalTimeSinceGPSWeekNs, |
||||
int dayOfYear1To366, |
||||
int gpsWeekNumber, |
||||
double[] positionVelocitySolutionEcef, |
||||
double[] positionVelocityUncertaintyEnu, |
||||
double[] pseudorangeResidualMeters) |
||||
throws Exception { |
||||
|
||||
List<GpsMeasurementWithRangeAndUncertainty> usefulSatellitesToPseudorangeMeasurements = |
||||
UserPositionVelocityWeightedLeastSquare.computePseudorangeAndUncertainties( |
||||
Arrays.asList(usefulSatellitesToReceiverMeasurements), |
||||
usefulSatellitesToTOWNs, |
||||
largestTowNs); |
||||
|
||||
// calculate iterative least square position solution and velocity solutions
|
||||
userPositionVelocityLeastSquare.calculateUserPositionVelocityLeastSquare( |
||||
mGpsNavMessageProtoUsed, |
||||
usefulSatellitesToPseudorangeMeasurements, |
||||
arrivalTimeSinceGPSWeekNs * SECONDS_PER_NANO, |
||||
gpsWeekNumber, |
||||
dayOfYear1To366, |
||||
positionVelocitySolutionEcef, |
||||
positionVelocityUncertaintyEnu, |
||||
pseudorangeResidualMeters); |
||||
|
||||
Log.d( |
||||
TAG, |
||||
"Least Square Position Solution in ECEF meters: " |
||||
+ positionVelocitySolutionEcef[0] |
||||
+ " " |
||||
+ positionVelocitySolutionEcef[1] |
||||
+ " " |
||||
+ positionVelocitySolutionEcef[2]); |
||||
Log.d(TAG, "Estimated Receiver clock offset in meters: " + positionVelocitySolutionEcef[3]); |
||||
|
||||
Log.d( |
||||
TAG, |
||||
"Velocity Solution in ECEF Mps: " |
||||
+ positionVelocitySolutionEcef[4] |
||||
+ " " |
||||
+ positionVelocitySolutionEcef[5] |
||||
+ " " |
||||
+ positionVelocitySolutionEcef[6]); |
||||
Log.d(TAG, "Estimated Reciever clock offset rate in mps: " + positionVelocitySolutionEcef[7]); |
||||
} |
||||
|
||||
/** |
||||
* Reads the navigation message from the SUPL server by creating a Stubby client to Stubby server |
||||
* that wraps the SUPL server. The input is the time in nanoseconds since the GPS epoch at which |
||||
* the navigation message is required and the output is a {@link GpsNavMessageProto} |
||||
* |
||||
* @throws IOException |
||||
* @throws UnknownHostException |
||||
*/ |
||||
private GpsNavMessageProto getSuplNavMessage(long latE7, long lngE7) |
||||
throws UnknownHostException, IOException { |
||||
SuplRrlpController suplRrlpController = |
||||
new SuplRrlpController(SUPL_SERVER_NAME, SUPL_SERVER_PORT); |
||||
GpsNavMessageProto navMessageProto = suplRrlpController.generateNavMessage(latE7, lngE7); |
||||
|
||||
return navMessageProto; |
||||
} |
||||
|
||||
/** |
||||
* Checks if we should continue using the navigation message from the SUPL server, or use the |
||||
* navigation message from the device if we fully received it. If the navigation message read from |
||||
* the receiver has all the visible satellite ephemerides, return false, otherwise, return true. |
||||
*/ |
||||
private static boolean continueUsingNavMessageFromSupl( |
||||
GpsMeasurement[] usefulSatellitesToReceiverMeasurements, |
||||
GpsNavMessageProto hardwareGpsNavMessageProto) { |
||||
boolean useNavMessageFromSupl = true; |
||||
if (hardwareGpsNavMessageProto != null) { |
||||
ArrayList<GpsEphemerisProto> hardwareEphemeridesList= |
||||
new ArrayList<GpsEphemerisProto>(Arrays.asList(hardwareGpsNavMessageProto.ephemerids)); |
||||
if (hardwareGpsNavMessageProto.iono != null) { |
||||
for (int i = 0; i < GpsNavigationMessageStore.MAX_NUMBER_OF_SATELLITES; i++) { |
||||
if (usefulSatellitesToReceiverMeasurements[i] != null) { |
||||
int prn = i + 1; |
||||
for (GpsEphemerisProto hardwareEphProtoFromList : hardwareEphemeridesList) { |
||||
if (hardwareEphProtoFromList.prn == prn) { |
||||
useNavMessageFromSupl = false; |
||||
break; |
||||
} |
||||
useNavMessageFromSupl = true; |
||||
} |
||||
if (useNavMessageFromSupl == true) { |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return useNavMessageFromSupl; |
||||
} |
||||
|
||||
/** |
||||
* Parses a string array containing an updates to the navigation message and return the most |
||||
* recent {@link GpsNavMessageProto}. |
||||
*/ |
||||
public void parseHwNavigationMessageUpdates(GnssNavigationMessage navigationMessage) { |
||||
byte messagePrn = (byte) navigationMessage.getSvid(); |
||||
byte messageType = (byte) (navigationMessage.getType() >> 8); |
||||
int subMessageId = navigationMessage.getSubmessageId(); |
||||
|
||||
byte[] messageRawData = navigationMessage.getData(); |
||||
// parse only GPS navigation messages for now
|
||||
if (messageType == 1) { |
||||
mGpsNavigationMessageStore.onNavMessageReported( |
||||
messagePrn, messageType, (short) subMessageId, messageRawData); |
||||
mHardwareGpsNavMessageProto = mGpsNavigationMessageStore.createDecodedNavMessage(); |
||||
} |
||||
|
||||
} |
||||
|
||||
/** Sets a rough location of the receiver that can be used to request SUPL assistance data */ |
||||
public void setReferencePosition(int latE7, int lngE7, int altE7) { |
||||
if (mReferenceLocation == null) { |
||||
mReferenceLocation = new int[3]; |
||||
} |
||||
mReferenceLocation[0] = latE7; |
||||
mReferenceLocation[1] = lngE7; |
||||
mReferenceLocation[2] = altE7; |
||||
} |
||||
|
||||
/** |
||||
* Converts the input from LLA coordinates to ECEF and set up the reference position of |
||||
* {@code mUserPositionVelocityLeastSquareCalculator} to calculate a corrected residual. |
||||
* |
||||
* <p> Based on this input ground truth, true residuals can be computed. This is done by using |
||||
* the high elevation satellites to compute the true user clock error and with the knowledge of |
||||
* the satellite positions. |
||||
* |
||||
* <p> If no ground truth is set, no residual analysis will be performed. |
||||
*/ |
||||
public void setCorrectedResidualComputationTruthLocationLla |
||||
(double[] groundTruthLocationLla) { |
||||
if (groundTruthLocationLla == null) { |
||||
mUserPositionVelocityLeastSquareCalculator |
||||
.setTruthLocationForCorrectedResidualComputationEcef(null); |
||||
return; |
||||
} |
||||
GeodeticLlaValues llaValues = |
||||
new GeodeticLlaValues( |
||||
Math.toRadians(groundTruthLocationLla[0]), |
||||
Math.toRadians(groundTruthLocationLla[1]), |
||||
Math.toRadians(groundTruthLocationLla[2])); |
||||
mUserPositionVelocityLeastSquareCalculator.setTruthLocationForCorrectedResidualComputationEcef( |
||||
Lla2EcefConverter.convertFromLlaToEcefMeters(llaValues)); |
||||
} |
||||
|
||||
/** Returns the last computed weighted least square position solution */ |
||||
public double[] getPositionSolutionLatLngDeg() { |
||||
return mPositionSolutionLatLngDeg; |
||||
} |
||||
|
||||
/** Returns the last computed Velocity solution */ |
||||
public double[] getVelocitySolutionEnuMps() { |
||||
return mVelocitySolutionEnuMps; |
||||
} |
||||
|
||||
/** |
||||
* Returns the last computed position and velocity uncertainties in meters and meter per seconds, |
||||
* respectively. |
||||
*/ |
||||
public double[] getPositionVelocityUncertaintyEnu() { |
||||
return mPositionVelocityUncertaintyEnu; |
||||
} |
||||
|
||||
/** |
||||
* Returns the pseudorange residuals corrected by using clock bias computed from highest |
||||
* elevationDegree satellites. |
||||
*/ |
||||
public double[] getPseudorangeResidualsMeters() { |
||||
return mPseudorangeResidualsMeters; |
||||
} |
||||
} |
@ -0,0 +1,38 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.location.lbs.gnss.gps.pseudorange; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* Interface for smoothing a list of {@link GpsMeasurementWithRangeAndUncertainty} instances |
||||
* received at a point of time. |
||||
*/ |
||||
interface PseudorangeSmoother { |
||||
|
||||
/** |
||||
* Takes an input list of {@link GpsMeasurementWithRangeAndUncertainty} instances and returns a |
||||
* new list that contains smoothed pseudorange measurements. |
||||
* |
||||
* <p>The input list is of size {@link GpsNavigationMessageStore#MAX_NUMBER_OF_SATELLITES} with |
||||
* not visible GPS satellites having null entries, and the returned new list is of the same size. |
||||
* |
||||
* <p>The method does not modify the input list. |
||||
*/ |
||||
List<GpsMeasurementWithRangeAndUncertainty> updatePseudorangeSmoothingResult( |
||||
List<GpsMeasurementWithRangeAndUncertainty> usefulSatellitesToGPSReceiverMeasurements); |
||||
} |
@ -0,0 +1,202 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.location.lbs.gnss.gps.pseudorange; |
||||
|
||||
import com.google.common.base.Preconditions; |
||||
import com.google.location.lbs.gnss.gps.pseudorange.EcefToTopocentricConverter.TopocentricAEDValues; |
||||
import com.google.location.lbs.gnss.gps.pseudorange.UserPositionVelocityWeightedLeastSquare. |
||||
SatellitesPositionPseudorangesResidualAndCovarianceMatrix; |
||||
import java.util.Arrays; |
||||
import java.util.Comparator; |
||||
|
||||
/** |
||||
* A tool with the methods to perform the pseudorange residual analysis. |
||||
* |
||||
* <p>The tool allows correcting the pseudorange residuals computed in WLS by removing the user |
||||
* clock error. The user clock bias is computed using the highest elevation satellites as those are |
||||
* assumed not to suffer from multipath. The reported residuals are provided at the input ground |
||||
* truth position by applying an adjustment using the distance of WLS to satellites vs ground-truth |
||||
* to satellites. |
||||
*/ |
||||
|
||||
public class ResidualCorrectionCalculator { |
||||
|
||||
/** |
||||
* The threshold for the residual of user clock bias per satellite with respect to the best user |
||||
* clock bias. |
||||
*/ |
||||
private static final double BEST_USER_CLOCK_BIAS_RESIDUAL_THRESHOLD_METERS = 10; |
||||
|
||||
/* The number of satellites we pick for calculating the best user clock bias */ |
||||
private static final int MIN_SATS_FOR_BIAS_COMPUTATION = 4; |
||||
|
||||
/** |
||||
* Corrects the pseudorange residual by the best user clock bias estimation computed from the top |
||||
* elevation satellites. |
||||
* |
||||
* @param satellitesPositionPseudorangesResidual satellite position and pseudorange residual info |
||||
* passed in from WLS |
||||
* @param positionVelocitySolutionECEF position velocity solution passed in from WLS |
||||
* @param groundTruthInputECEFMeters the reference position in ECEF meters |
||||
* @return an array contains the corrected pseusorange residual in meters for each satellite |
||||
*/ |
||||
public static double[] calculateCorrectedResiduals( |
||||
SatellitesPositionPseudorangesResidualAndCovarianceMatrix |
||||
satellitesPositionPseudorangesResidual, |
||||
double[] positionVelocitySolutionECEF, |
||||
double[] groundTruthInputECEFMeters) { |
||||
|
||||
|
||||
double[] residuals = satellitesPositionPseudorangesResidual.pseudorangeResidualsMeters.clone(); |
||||
int[] satellitePrn = satellitesPositionPseudorangesResidual.satellitePRNs.clone(); |
||||
double[] satelliteElevationDegree = new double[residuals.length]; |
||||
SatelliteElevationAndResiduals[] satelliteResidualsListAndElevation = |
||||
new SatelliteElevationAndResiduals[residuals.length]; |
||||
|
||||
// Check the alignment between inputs
|
||||
Preconditions.checkArgument(residuals.length == satellitePrn.length); |
||||
|
||||
// Apply residual corrections per satellite
|
||||
for (int i = 0; i < residuals.length; i++) { |
||||
// Calculate the delta of user-satellite distance between ground truth and WLS solution
|
||||
// and use the delta to adjust the residuals computed from the WLS. With this adjustments all
|
||||
// residuals will be as if they are computed with respect to the ground truth rather than
|
||||
// the WLS.
|
||||
double[] satellitePos = satellitesPositionPseudorangesResidual.satellitesPositionsMeters[i]; |
||||
double wlsUserSatelliteDistance = |
||||
GpsMathOperations.vectorNorm( |
||||
GpsMathOperations.subtractTwoVectors( |
||||
Arrays.copyOf(positionVelocitySolutionECEF, 3), |
||||
satellitePos)); |
||||
double groundTruthSatelliteDistance = |
||||
GpsMathOperations.vectorNorm( |
||||
GpsMathOperations.subtractTwoVectors(groundTruthInputECEFMeters, satellitePos)); |
||||
|
||||
// Compute the adjustment for satellite i
|
||||
double groundTruthAdjustment = wlsUserSatelliteDistance - groundTruthSatelliteDistance; |
||||
|
||||
// Correct the input residual with the adjustment to ground truth
|
||||
residuals[i] = residuals[i] - groundTruthAdjustment; |
||||
|
||||
// Calculate the elevation in degrees of satellites
|
||||
TopocentricAEDValues topocentricAedValues = |
||||
EcefToTopocentricConverter.calculateElAzDistBetween2Points( |
||||
groundTruthInputECEFMeters, satellitesPositionPseudorangesResidual. |
||||
satellitesPositionsMeters[i] |
||||
); |
||||
|
||||
satelliteElevationDegree[i] = Math.toDegrees(topocentricAedValues.elevationRadians); |
||||
|
||||
// Store the computed satellite elevations and residuals into a SatelliteElevationAndResiduals
|
||||
// list with clock correction removed.
|
||||
satelliteResidualsListAndElevation[i] = |
||||
new SatelliteElevationAndResiduals( |
||||
satelliteElevationDegree[i], residuals[i] |
||||
+ positionVelocitySolutionECEF[3], satellitePrn[i]); |
||||
} |
||||
|
||||
double bestUserClockBiasMeters = calculateBestUserClockBias(satelliteResidualsListAndElevation); |
||||
|
||||
// Use the best clock bias to correct the residuals to ensure that the receiver clock errors are
|
||||
// removed from the reported residuals in the analysis
|
||||
double[] correctedResidualsMeters = |
||||
GpsMathOperations.createAndFillArray( |
||||
GpsNavigationMessageStore.MAX_NUMBER_OF_SATELLITES, Double.NaN |
||||
); |
||||
|
||||
for (SatelliteElevationAndResiduals element : |
||||
satelliteResidualsListAndElevation) { |
||||
correctedResidualsMeters[element.svID - 1] = element.residual - bestUserClockBiasMeters; |
||||
} |
||||
|
||||
return correctedResidualsMeters; |
||||
} |
||||
|
||||
/** |
||||
* Computes the user clock bias by iteratively averaging the clock bias of top elevation |
||||
* satellites. |
||||
* |
||||
* @param satelliteResidualsAndElevationList a list of satellite elevation and |
||||
* pseudorange residuals |
||||
* @return the corrected best user clock bias |
||||
*/ |
||||
private static double calculateBestUserClockBias( |
||||
SatelliteElevationAndResiduals[] satelliteResidualsAndElevationList) { |
||||
|
||||
// Sort the satellites by descending order of their elevations
|
||||
Arrays.sort( |
||||
satelliteResidualsAndElevationList, |
||||
new Comparator<SatelliteElevationAndResiduals>() { |
||||
@Override |
||||
public int compare( |
||||
SatelliteElevationAndResiduals o1, SatelliteElevationAndResiduals o2) { |
||||
return Double.compare(o2.elevationDegree, o1.elevationDegree); |
||||
} |
||||
}); |
||||
|
||||
// Pick up the top elevation satellites
|
||||
double[] topElevationSatsResiduals = GpsMathOperations.createAndFillArray( |
||||
MIN_SATS_FOR_BIAS_COMPUTATION, Double.NaN |
||||
); |
||||
int numOfUsefulSatsToComputeBias = 0; |
||||
for (int i = 0; i < satelliteResidualsAndElevationList.length |
||||
&& i < topElevationSatsResiduals.length; i++) { |
||||
topElevationSatsResiduals[i] = satelliteResidualsAndElevationList[i].residual; |
||||
numOfUsefulSatsToComputeBias++; |
||||
} |
||||
|
||||
double meanResidual; |
||||
double[] deltaResidualFromMean; |
||||
int maxDeltaIndex = -1; |
||||
|
||||
// Iteratively remove the satellites with highest residuals with respect to the mean of the
|
||||
// residuals until the highest residual in the list is below threshold.
|
||||
do { |
||||
if (maxDeltaIndex >= 0) { |
||||
topElevationSatsResiduals[maxDeltaIndex] = Double.NaN; |
||||
numOfUsefulSatsToComputeBias--; |
||||
} |
||||
meanResidual = GpsMathOperations.meanOfVector(topElevationSatsResiduals); |
||||
deltaResidualFromMean |
||||
= GpsMathOperations.subtractByScalar(topElevationSatsResiduals, meanResidual); |
||||
maxDeltaIndex = GpsMathOperations.maxIndexOfVector(deltaResidualFromMean); |
||||
} while (deltaResidualFromMean[maxDeltaIndex] > BEST_USER_CLOCK_BIAS_RESIDUAL_THRESHOLD_METERS |
||||
&& numOfUsefulSatsToComputeBias > 2); |
||||
|
||||
return meanResidual; |
||||
} |
||||
|
||||
/** A container for satellite residual and elevationDegree information */ |
||||
private static class SatelliteElevationAndResiduals { |
||||
/** Satellite pseudorange or pseudorange rate residual with clock correction removed */ |
||||
final double residual; |
||||
|
||||
/** Satellite elevation in degrees with respect to the user */ |
||||
final double elevationDegree; |
||||
|
||||
/** Satellite ID */ |
||||
final int svID; |
||||
|
||||
SatelliteElevationAndResiduals( |
||||
double elevationDegree, double residual, int svID) { |
||||
this.residual = residual; |
||||
this.svID = svID; |
||||
this.elevationDegree = elevationDegree; |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,193 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.location.lbs.gnss.gps.pseudorange; |
||||
|
||||
import android.location.cts.nano.Ephemeris.GpsEphemerisProto; |
||||
/** |
||||
* Calculates the GPS satellite clock correction based on parameters observed from the navigation |
||||
* message |
||||
* <p>Source: Page 88 - 90 of the ICD-GPS 200 |
||||
*/ |
||||
public class SatelliteClockCorrectionCalculator { |
||||
private static final double SPEED_OF_LIGHT_MPS = 299792458.0; |
||||
private static final double EARTH_UNIVERSAL_GRAVITATIONAL_CONSTANT_M3_SM2 = 3.986005e14; |
||||
private static final double RELATIVISTIC_CONSTANT_F = -4.442807633e-10; |
||||
private static final int SECONDS_IN_WEEK = 604800; |
||||
private static final double ACCURACY_TOLERANCE = 1.0e-11; |
||||
private static final int MAX_ITERATIONS = 100; |
||||
|
||||
/** |
||||
* Computes the GPS satellite clock correction term in meters iteratively following page 88 - 90 |
||||
* and 98 - 100 of the ICD GPS 200. The method returns a pair of satellite clock correction in |
||||
* meters and Kepler Eccentric Anomaly in Radians. |
||||
* |
||||
* @param ephemerisProto parameters of the navigation message |
||||
* @param receiverGpsTowAtTimeOfTransmission Reciever estimate of GPS time of week when signal was |
||||
* transmitted (seconds) |
||||
* @param receiverGpsWeekAtTimeOfTrasnmission Receiver estimate of GPS week when signal was |
||||
* transmitted (0-1024+) |
||||
* @throws Exception |
||||
*/ |
||||
|
||||
public static SatClockCorrection calculateSatClockCorrAndEccAnomAndTkIteratively( |
||||
GpsEphemerisProto ephemerisProto, double receiverGpsTowAtTimeOfTransmission, |
||||
double receiverGpsWeekAtTimeOfTrasnmission) throws Exception { |
||||
// Units are not added in the variable names to have the same name as the ICD-GPS200
|
||||
// Mean anomaly (radians)
|
||||
double meanAnomalyRad; |
||||
// Kepler's Equation for Eccentric Anomaly iteratively (Radians)
|
||||
double eccentricAnomalyRad; |
||||
// Semi-major axis of orbit (meters)
|
||||
double a = ephemerisProto.rootOfA * ephemerisProto.rootOfA; |
||||
// Computed mean motion (radians/seconds)
|
||||
double n0 = Math.sqrt(EARTH_UNIVERSAL_GRAVITATIONAL_CONSTANT_M3_SM2 / (a * a * a)); |
||||
// Corrected mean motion (radians/seconds)
|
||||
double n = n0 + ephemerisProto.deltaN; |
||||
// In the following, Receiver GPS week and ephemeris GPS week are used to correct for week
|
||||
// rollover when calculating the time from clock reference epoch (tcSec)
|
||||
double timeOfTransmissionIncludingRxWeekSec = |
||||
receiverGpsWeekAtTimeOfTrasnmission * SECONDS_IN_WEEK + receiverGpsTowAtTimeOfTransmission; |
||||
// time from clock reference epoch (seconds) page 88 ICD-GPS200
|
||||
double tcSec = timeOfTransmissionIncludingRxWeekSec |
||||
- (ephemerisProto.week * SECONDS_IN_WEEK + ephemerisProto.toc); |
||||
// Correction for week rollover
|
||||
tcSec = fixWeekRollover(tcSec); |
||||
double oldEcentricAnomalyRad = 0.0; |
||||
double newSatClockCorrectionSeconds = 0.0; |
||||
double relativisticCorrection = 0.0; |
||||
double changeInSatClockCorrection = 0.0; |
||||
// Initial satellite clock correction (unknown relativistic correction). Iterate to correct
|
||||
// with the relativistic effect and obtain a stable
|
||||
final double initSatClockCorrectionSeconds = ephemerisProto.af0 |
||||
+ ephemerisProto.af1 * tcSec |
||||
+ ephemerisProto.af2 * tcSec * tcSec - ephemerisProto.tgd; |
||||
double satClockCorrectionSeconds = initSatClockCorrectionSeconds; |
||||
double tkSec; |
||||
int satClockCorrectionsCounter = 0; |
||||
do { |
||||
int eccentricAnomalyCounter = 0; |
||||
// time from ephemeris reference epoch (seconds) page 98 ICD-GPS200
|
||||
tkSec = timeOfTransmissionIncludingRxWeekSec - ( |
||||
ephemerisProto.week * SECONDS_IN_WEEK + ephemerisProto.toe |
||||
+ satClockCorrectionSeconds); |
||||
// Correction for week rollover
|
||||
tkSec = fixWeekRollover(tkSec); |
||||
// Mean anomaly (radians)
|
||||
meanAnomalyRad = ephemerisProto.m0 + n * tkSec; |
||||
// eccentric anomaly (radians)
|
||||
eccentricAnomalyRad = meanAnomalyRad; |
||||
// Iteratively solve for Kepler's eccentric anomaly according to ICD-GPS200 page 99
|
||||
do { |
||||
oldEcentricAnomalyRad = eccentricAnomalyRad; |
||||
eccentricAnomalyRad = |
||||
meanAnomalyRad + ephemerisProto.e * Math.sin(eccentricAnomalyRad); |
||||
eccentricAnomalyCounter++; |
||||
if (eccentricAnomalyCounter > MAX_ITERATIONS) { |
||||
throw new Exception("Kepler Eccentric Anomaly calculation did not converge in " |
||||
+ MAX_ITERATIONS + " iterations"); |
||||
} |
||||
} while (Math.abs(oldEcentricAnomalyRad - eccentricAnomalyRad) > ACCURACY_TOLERANCE); |
||||
// relativistic correction term (seconds)
|
||||
relativisticCorrection = RELATIVISTIC_CONSTANT_F * ephemerisProto.e |
||||
* ephemerisProto.rootOfA * Math.sin(eccentricAnomalyRad); |
||||
// satellite clock correction including relativistic effect
|
||||
newSatClockCorrectionSeconds = initSatClockCorrectionSeconds + relativisticCorrection; |
||||
changeInSatClockCorrection = |
||||
Math.abs(satClockCorrectionSeconds - newSatClockCorrectionSeconds); |
||||
satClockCorrectionSeconds = newSatClockCorrectionSeconds; |
||||
satClockCorrectionsCounter++; |
||||
if (satClockCorrectionsCounter > MAX_ITERATIONS) { |
||||
throw new Exception("Satellite Clock Correction calculation did not converge in " |
||||
+ MAX_ITERATIONS + " iterations"); |
||||
} |
||||
} while (changeInSatClockCorrection > ACCURACY_TOLERANCE); |
||||
tkSec = timeOfTransmissionIncludingRxWeekSec - ( |
||||
ephemerisProto.week * SECONDS_IN_WEEK + ephemerisProto.toe |
||||
+ satClockCorrectionSeconds); |
||||
// return satellite clock correction (meters) and Kepler Eccentric Anomaly in Radians
|
||||
return new SatClockCorrection(satClockCorrectionSeconds * SPEED_OF_LIGHT_MPS, |
||||
eccentricAnomalyRad, tkSec); |
||||
} |
||||
|
||||
/** |
||||
* Calculates Satellite Clock Error Rate in (meters/second) by subtracting the Satellite |
||||
* Clock Error Values at t+0.5s and t-0.5s. |
||||
* |
||||
* <p>This approximation is more accurate than differentiating because both the orbital |
||||
* and relativity terms have non-linearities that are not easily differentiable. |
||||
*/ |
||||
public static double calculateSatClockCorrErrorRate( |
||||
GpsEphemerisProto ephemerisProto, double receiverGpsTowAtTimeOfTransmissionSeconds, |
||||
double receiverGpsWeekAtTimeOfTrasnmission) throws Exception { |
||||
SatClockCorrection satClockCorrectionPlus = calculateSatClockCorrAndEccAnomAndTkIteratively( |
||||
ephemerisProto, receiverGpsTowAtTimeOfTransmissionSeconds + 0.5, |
||||
receiverGpsWeekAtTimeOfTrasnmission); |
||||
SatClockCorrection satClockCorrectionMinus = calculateSatClockCorrAndEccAnomAndTkIteratively( |
||||
ephemerisProto, receiverGpsTowAtTimeOfTransmissionSeconds - 0.5, |
||||
receiverGpsWeekAtTimeOfTrasnmission); |
||||
double satelliteClockErrorRate = satClockCorrectionPlus.satelliteClockCorrectionMeters |
||||
- satClockCorrectionMinus.satelliteClockCorrectionMeters; |
||||
return satelliteClockErrorRate; |
||||
} |
||||
|
||||
/** |
||||
* Method to check for week rollover according to ICD-GPS 200 page 98. |
||||
* |
||||
* <p>Result should be between -302400 and 302400 if the ephemeris is within one week of |
||||
* transmission, otherwise it is adjusted to the correct range |
||||
*/ |
||||
private static double fixWeekRollover(double time) { |
||||
double correctedTime = time; |
||||
if (time > SECONDS_IN_WEEK / 2.0) { |
||||
correctedTime = time - SECONDS_IN_WEEK; |
||||
} |
||||
if (time < -SECONDS_IN_WEEK / 2.0) { |
||||
correctedTime = time + SECONDS_IN_WEEK; |
||||
} |
||||
return correctedTime; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* Class containing the satellite clock correction parameters: The satellite clock correction in |
||||
* meters, Kepler Eccentric Anomaly in Radians and the time from the reference epoch in seconds. |
||||
*/ |
||||
public static class SatClockCorrection { |
||||
/** |
||||
* Satellite clock correction in meters |
||||
*/ |
||||
public final double satelliteClockCorrectionMeters; |
||||
/** |
||||
* Kepler Eccentric Anomaly in Radians |
||||
*/ |
||||
public final double eccentricAnomalyRadians; |
||||
/** |
||||
* Time from the reference epoch in Seconds |
||||
*/ |
||||
public final double timeFromRefEpochSec; |
||||
|
||||
/** |
||||
* Constructor |
||||
*/ |
||||
public SatClockCorrection(double satelliteClockCorrectionMeters, double eccentricAnomalyRadians, |
||||
double timeFromRefEpochSec) { |
||||
this.satelliteClockCorrectionMeters = satelliteClockCorrectionMeters; |
||||
this.eccentricAnomalyRadians = eccentricAnomalyRadians; |
||||
this.timeFromRefEpochSec = timeFromRefEpochSec; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,323 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.location.lbs.gnss.gps.pseudorange; |
||||
|
||||
import com.google.location.lbs.gnss.gps.pseudorange.SatelliteClockCorrectionCalculator.SatClockCorrection; |
||||
import android.location.cts.nano.Ephemeris.GpsEphemerisProto; |
||||
|
||||
/** |
||||
* Class to calculate GPS satellite positions from the ephemeris data |
||||
*/ |
||||
public class SatellitePositionCalculator { |
||||
private static final double SPEED_OF_LIGHT_MPS = 299792458.0; |
||||
private static final double UNIVERSAL_GRAVITATIONAL_PARAMETER_M3_SM2 = 3.986005e14; |
||||
private static final int NUMBER_OF_ITERATIONS_FOR_SAT_POS_CALCULATION = 5; |
||||
private static final double EARTH_ROTATION_RATE_RAD_PER_SEC = 7.2921151467e-5; |
||||
|
||||
/** |
||||
* |
||||
* Calculates GPS satellite position and velocity from ephemeris including the Sagnac effect |
||||
* starting from unknown user to satellite distance and speed. So we start from an initial guess |
||||
* of the user to satellite range and range rate and iterate to include the Sagnac effect. Few |
||||
* iterations are enough to achieve a satellite position with millimeter accuracy. |
||||
* A {@code PositionAndVelocity} class is returned containing satellite position in meters |
||||
* (x, y and z) and velocity in meters per second (x, y, z) |
||||
* |
||||
* <p>Satelite position and velocity equations are obtained from: |
||||
* http://www.gps.gov/technical/icwg/ICD-GPS-200C.pdf) pages 94 - 101 and
|
||||
* http://fenrir.naruoka.org/download/autopilot/note/080205_gps/gps_velocity.pdf
|
||||
* |
||||
* @param ephemerisProto parameters of the navigation message |
||||
* @param receiverGpsTowAtTimeOfTransmissionCorrectedSec Receiver estimate of GPS time of week |
||||
* when signal was transmitted corrected with the satellite clock drift (seconds) |
||||
* @param receiverGpsWeekAtTimeOfTransmission Receiver estimate of GPS week when signal was |
||||
* transmitted (0-1024+) |
||||
* @param userPosXMeters Last known user x-position (if known) [meters] |
||||
* @param userPosYMeters Last known user y-position (if known) [meters] |
||||
* @param userPosZMeters Last known user z-position (if known) [meters] |
||||
* @throws Exception |
||||
*/ |
||||
public static PositionAndVelocity calculateSatellitePositionAndVelocityFromEphemeris |
||||
(GpsEphemerisProto ephemerisProto, double receiverGpsTowAtTimeOfTransmissionCorrectedSec, |
||||
int receiverGpsWeekAtTimeOfTransmission, |
||||
double userPosXMeters, |
||||
double userPosYMeters, |
||||
double userPosZMeters) throws Exception { |
||||
|
||||
// lets start with a first user to sat distance guess of 70 ms and zero velocity
|
||||
RangeAndRangeRate userSatRangeAndRate = new RangeAndRangeRate |
||||
(0.070 * SPEED_OF_LIGHT_MPS, 0.0 /* range rate*/); |
||||
|
||||
// To apply sagnac effect correction, We are starting from an approximate guess of the user to
|
||||
// satellite range, iterate 3 times and that should be enough to reach millimeter accuracy
|
||||
PositionAndVelocity satPosAndVel = new PositionAndVelocity(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); |
||||
PositionAndVelocity userPosAndVel = |
||||
new PositionAndVelocity(userPosXMeters, userPosYMeters, userPosZMeters, |
||||
0.0 /* user velocity x*/, 0.0 /* user velocity y*/, 0.0 /* user velocity z */); |
||||
for (int i = 0; i < NUMBER_OF_ITERATIONS_FOR_SAT_POS_CALCULATION; i++) { |
||||
calculateSatellitePositionAndVelocity(ephemerisProto, |
||||
receiverGpsTowAtTimeOfTransmissionCorrectedSec, receiverGpsWeekAtTimeOfTransmission, |
||||
userSatRangeAndRate, satPosAndVel); |
||||
computeUserToSatelliteRangeAndRangeRate(userPosAndVel, satPosAndVel, userSatRangeAndRate); |
||||
} |
||||
return satPosAndVel; |
||||
} |
||||
|
||||
/** |
||||
* Calculates GPS satellite position and velocity from ephemeris based on the ICD-GPS-200. |
||||
* Satellite position in meters (x, y and z) and velocity in meters per second (x, y, z) are set |
||||
* in the passed {@code PositionAndVelocity} instance. |
||||
* |
||||
* <p>Sources: http://www.gps.gov/technical/icwg/ICD-GPS-200C.pdf) pages 94 - 101 and
|
||||
* http://fenrir.naruoka.org/download/autopilot/note/080205_gps/gps_velocity.pdf
|
||||
* |
||||
* @param ephemerisProto parameters of the navigation message |
||||
* @param receiverGpsTowAtTimeOfTransmissionCorrected Receiver estimate of GPS time of week when |
||||
* signal was transmitted corrected with the satellite clock drift (seconds) |
||||
* @param receiverGpsWeekAtTimeOfTransmission Receiver estimate of GPS week when signal was |
||||
* transmitted (0-1024+) |
||||
* @param userSatRangeAndRate user to satellite range and range rate |
||||
* @param satPosAndVel Satellite position and velocity instance in which the method results will |
||||
* be set |
||||
* @throws Exception |
||||
*/ |
||||
public static void calculateSatellitePositionAndVelocity(GpsEphemerisProto ephemerisProto, |
||||
double receiverGpsTowAtTimeOfTransmissionCorrected, int receiverGpsWeekAtTimeOfTransmission, |
||||
RangeAndRangeRate userSatRangeAndRate, PositionAndVelocity satPosAndVel) throws Exception { |
||||
|
||||
// Calculate satellite clock correction (meters), Kepler Eccentric anomaly (radians) and time
|
||||
// from ephemeris refrence epoch (tkSec) iteratively
|
||||
SatClockCorrection satClockCorrectionValues = |
||||
SatelliteClockCorrectionCalculator.calculateSatClockCorrAndEccAnomAndTkIteratively( |
||||
ephemerisProto, receiverGpsTowAtTimeOfTransmissionCorrected, |
||||
receiverGpsWeekAtTimeOfTransmission); |
||||
|
||||
double eccentricAnomalyRadians = satClockCorrectionValues.eccentricAnomalyRadians; |
||||
double tkSec = satClockCorrectionValues.timeFromRefEpochSec; |
||||
|
||||
// True_anomaly (angle from perigee)
|
||||
double trueAnomalyRadians = Math.atan2( |
||||
Math.sqrt(1.0 - ephemerisProto.e * ephemerisProto.e) |
||||
* Math.sin(eccentricAnomalyRadians), |
||||
Math.cos(eccentricAnomalyRadians) - ephemerisProto.e); |
||||
|
||||
// Argument of latitude of the satellite
|
||||
double argumentOfLatitudeRadians = trueAnomalyRadians + ephemerisProto.omega; |
||||
|
||||
// Radius of satellite orbit
|
||||
double radiusOfSatelliteOrbitMeters = ephemerisProto.rootOfA * ephemerisProto.rootOfA |
||||
* (1.0 - ephemerisProto.e * Math.cos(eccentricAnomalyRadians)); |
||||
|
||||
// Radius correction due to second harmonic perturbations of the orbit
|
||||
double radiusCorrectionMeters = ephemerisProto.crc |
||||
* Math.cos(2.0 * argumentOfLatitudeRadians) + ephemerisProto.crs |
||||
* Math.sin(2.0 * argumentOfLatitudeRadians); |
||||
// Argument of latitude correction due to second harmonic perturbations of the orbit
|
||||
double argumentOfLatitudeCorrectionRadians = ephemerisProto.cuc |
||||
* Math.cos(2.0 * argumentOfLatitudeRadians) + ephemerisProto.cus |
||||
* Math.sin(2.0 * argumentOfLatitudeRadians); |
||||
// Correction to inclination due to second harmonic perturbations of the orbit
|
||||
double inclinationCorrectionRadians = ephemerisProto.cic |
||||
* Math.cos(2.0 * argumentOfLatitudeRadians) + ephemerisProto.cis |
||||
* Math.sin(2.0 * argumentOfLatitudeRadians); |
||||
|
||||
// Corrected radius of satellite orbit
|
||||
radiusOfSatelliteOrbitMeters += radiusCorrectionMeters; |
||||
// Corrected argument of latitude
|
||||
argumentOfLatitudeRadians += argumentOfLatitudeCorrectionRadians; |
||||
// Corrected inclination
|
||||
double inclinationRadians = |
||||
ephemerisProto.i0 + inclinationCorrectionRadians + ephemerisProto.iDot * tkSec; |
||||
|
||||
// Position in orbital plane
|
||||
double xPositionMeters = radiusOfSatelliteOrbitMeters * Math.cos(argumentOfLatitudeRadians); |
||||
double yPositionMeters = radiusOfSatelliteOrbitMeters * Math.sin(argumentOfLatitudeRadians); |
||||
|
||||
// Corrected longitude of the ascending node (signal propagation time is included to compensate
|
||||
// for the Sagnac effect)
|
||||
double omegaKRadians = ephemerisProto.omega0 |
||||
+ (ephemerisProto.omegaDot - EARTH_ROTATION_RATE_RAD_PER_SEC) * tkSec |
||||
- EARTH_ROTATION_RATE_RAD_PER_SEC |
||||
* (ephemerisProto.toe + userSatRangeAndRate.rangeMeters / SPEED_OF_LIGHT_MPS); |
||||
|
||||
// compute the resulting satellite position
|
||||
double satPosXMeters = xPositionMeters * Math.cos(omegaKRadians) - yPositionMeters |
||||
* Math.cos(inclinationRadians) * Math.sin(omegaKRadians); |
||||
double satPosYMeters = xPositionMeters * Math.sin(omegaKRadians) + yPositionMeters |
||||
* Math.cos(inclinationRadians) * Math.cos(omegaKRadians); |
||||
double satPosZMeters = yPositionMeters * Math.sin(inclinationRadians); |
||||
|
||||
// Satellite Velocity Computation using the broadcast ephemeris
|
||||
// http://fenrir.naruoka.org/download/autopilot/note/080205_gps/gps_velocity.pdf
|
||||
// Units are not added in some of the variable names to have the same name as the ICD-GPS200
|
||||
// Semi-major axis of orbit (meters)
|
||||
double a = ephemerisProto.rootOfA * ephemerisProto.rootOfA; |
||||
// Computed mean motion (radians/seconds)
|
||||
double n0 = Math.sqrt(UNIVERSAL_GRAVITATIONAL_PARAMETER_M3_SM2 / (a * a * a)); |
||||
// Corrected mean motion (radians/seconds)
|
||||
double n = n0 + ephemerisProto.deltaN; |
||||
// Derivative of mean anomaly (radians/seconds)
|
||||
double meanAnomalyDotRadPerSec = n; |
||||
// Derivative of eccentric anomaly (radians/seconds)
|
||||
double eccentricAnomalyDotRadPerSec = |
||||
meanAnomalyDotRadPerSec / (1.0 - ephemerisProto.e * Math.cos(eccentricAnomalyRadians)); |
||||
// Derivative of true anomaly (radians/seconds)
|
||||
double trueAnomalydotRadPerSec = Math.sin(eccentricAnomalyRadians) |
||||
* eccentricAnomalyDotRadPerSec |
||||
* (1.0 + ephemerisProto.e * Math.cos(trueAnomalyRadians)) / ( |
||||
Math.sin(trueAnomalyRadians) |
||||
* (1.0 - ephemerisProto.e * Math.cos(eccentricAnomalyRadians))); |
||||
// Derivative of argument of latitude (radians/seconds)
|
||||
double argumentOfLatitudeDotRadPerSec = trueAnomalydotRadPerSec + 2.0 * (ephemerisProto.cus |
||||
* Math.cos(2.0 * argumentOfLatitudeRadians) - ephemerisProto.cuc |
||||
* Math.sin(2.0 * argumentOfLatitudeRadians)) * trueAnomalydotRadPerSec; |
||||
// Derivative of radius of satellite orbit (m/s)
|
||||
double radiusOfSatelliteOrbitDotMPerSec = a * ephemerisProto.e |
||||
* Math.sin(eccentricAnomalyRadians) * n |
||||
/ (1.0 - ephemerisProto.e * Math.cos(eccentricAnomalyRadians)) + 2.0 * ( |
||||
ephemerisProto.crs * Math.cos(2.0 * argumentOfLatitudeRadians) |
||||
- ephemerisProto.crc * Math.sin(2.0 * argumentOfLatitudeRadians)) |
||||
* trueAnomalydotRadPerSec; |
||||
// Derivative of the inclination (radians/seconds)
|
||||
double inclinationDotRadPerSec = ephemerisProto.iDot + (ephemerisProto.cis |
||||
* Math.cos(2.0 * argumentOfLatitudeRadians) - ephemerisProto.cic |
||||
* Math.sin(2.0 * argumentOfLatitudeRadians)) * 2.0 * trueAnomalydotRadPerSec; |
||||
|
||||
double xVelocityMPS = radiusOfSatelliteOrbitDotMPerSec * Math.cos(argumentOfLatitudeRadians) |
||||
- yPositionMeters * argumentOfLatitudeDotRadPerSec; |
||||
double yVelocityMPS = radiusOfSatelliteOrbitDotMPerSec * Math.sin(argumentOfLatitudeRadians) |
||||
+ xPositionMeters * argumentOfLatitudeDotRadPerSec; |
||||
|
||||
// Corrected rate of right ascension including compensation for the Sagnac effect
|
||||
double omegaDotRadPerSec = ephemerisProto.omegaDot - EARTH_ROTATION_RATE_RAD_PER_SEC |
||||
* (1.0 + userSatRangeAndRate.rangeRateMetersPerSec / SPEED_OF_LIGHT_MPS); |
||||
// compute the resulting satellite velocity
|
||||
double satVelXMPS = |
||||
(xVelocityMPS - yPositionMeters * Math.cos(inclinationRadians) * omegaDotRadPerSec) |
||||
* Math.cos(omegaKRadians) - (xPositionMeters * omegaDotRadPerSec + yVelocityMPS |
||||
* Math.cos(inclinationRadians) - yPositionMeters * Math.sin(inclinationRadians) |
||||
* inclinationDotRadPerSec) * Math.sin(omegaKRadians); |
||||
double satVelYMPS = |
||||
(xVelocityMPS - yPositionMeters * Math.cos(inclinationRadians) * omegaDotRadPerSec) |
||||
* Math.sin(omegaKRadians) + (xPositionMeters * omegaDotRadPerSec + yVelocityMPS |
||||
* Math.cos(inclinationRadians) - yPositionMeters * Math.sin(inclinationRadians) |
||||
* inclinationDotRadPerSec) * Math.cos(omegaKRadians); |
||||
double satVelZMPS = yVelocityMPS * Math.sin(inclinationRadians) + yPositionMeters |
||||
* Math.cos(inclinationRadians) * inclinationDotRadPerSec; |
||||
|
||||
satPosAndVel.positionXMeters = satPosXMeters; |
||||
satPosAndVel.positionYMeters = satPosYMeters; |
||||
satPosAndVel.positionZMeters = satPosZMeters; |
||||
satPosAndVel.velocityXMetersPerSec = satVelXMPS; |
||||
satPosAndVel.velocityYMetersPerSec = satVelYMPS; |
||||
satPosAndVel.velocityZMetersPerSec = satVelZMPS; |
||||
} |
||||
|
||||
/** |
||||
* Computes and sets the passed {@code RangeAndRangeRate} instance containing user to satellite |
||||
* range (meters) and range rate (m/s) given the user position (ECEF meters), user velocity (m/s), |
||||
* satellite position (ECEF meters) and satellite velocity (m/s). |
||||
*/ |
||||
private static void computeUserToSatelliteRangeAndRangeRate(PositionAndVelocity userPosAndVel, |
||||
PositionAndVelocity satPosAndVel, RangeAndRangeRate rangeAndRangeRate) { |
||||
double dXMeters = satPosAndVel.positionXMeters - userPosAndVel.positionXMeters; |
||||
double dYMeters = satPosAndVel.positionYMeters - userPosAndVel.positionYMeters; |
||||
double dZMeters = satPosAndVel.positionZMeters - userPosAndVel.positionZMeters; |
||||
// range in meters
|
||||
double rangeMeters = Math.sqrt(dXMeters * dXMeters + dYMeters * dYMeters + dZMeters * dZMeters); |
||||
// range rate in meters / second
|
||||
double rangeRateMetersPerSec = |
||||
((userPosAndVel.velocityXMetersPerSec - satPosAndVel.velocityXMetersPerSec) * dXMeters |
||||
+ (userPosAndVel.velocityYMetersPerSec - satPosAndVel.velocityYMetersPerSec) * dYMeters |
||||
+ (userPosAndVel.velocityZMetersPerSec - satPosAndVel.velocityZMetersPerSec) * dZMeters) |
||||
/ rangeMeters; |
||||
rangeAndRangeRate.rangeMeters = rangeMeters; |
||||
rangeAndRangeRate.rangeRateMetersPerSec = rangeRateMetersPerSec; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* A class containing position values (x, y, z) in meters and velocity values (x, y, z) in meters |
||||
* per seconds |
||||
*/ |
||||
public static class PositionAndVelocity { |
||||
/** |
||||
* x - position in meters |
||||
*/ |
||||
public double positionXMeters; |
||||
/** |
||||
* y - position in meters |
||||
*/ |
||||
public double positionYMeters; |
||||
/** |
||||
* z - position in meters |
||||
*/ |
||||
public double positionZMeters; |
||||
/** |
||||
* x - velocity in meters |
||||
*/ |
||||
public double velocityXMetersPerSec; |
||||
/** |
||||
* y - velocity in meters |
||||
*/ |
||||
public double velocityYMetersPerSec; |
||||
/** |
||||
* z - velocity in meters |
||||
*/ |
||||
public double velocityZMetersPerSec; |
||||
|
||||
/** |
||||
* Constructor |
||||
*/ |
||||
public PositionAndVelocity(double positionXMeters, |
||||
double positionYMeters, |
||||
double positionZMeters, |
||||
double velocityXMetersPerSec, |
||||
double velocityYMetersPerSec, |
||||
double velocityZMetersPerSec) { |
||||
this.positionXMeters = positionXMeters; |
||||
this.positionYMeters = positionYMeters; |
||||
this.positionZMeters = positionZMeters; |
||||
this.velocityXMetersPerSec = velocityXMetersPerSec; |
||||
this.velocityYMetersPerSec = velocityYMetersPerSec; |
||||
this.velocityZMetersPerSec = velocityZMetersPerSec; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* A class containing range of satellite to user in meters and range rate in meters per seconds |
||||
*/ |
||||
public static class RangeAndRangeRate { |
||||
/** |
||||
* Range in meters |
||||
*/ |
||||
public double rangeMeters; |
||||
/** |
||||
* Range rate in meters per seconds |
||||
*/ |
||||
public double rangeRateMetersPerSec; |
||||
|
||||
/** |
||||
* Constructor |
||||
*/ |
||||
public RangeAndRangeRate(double rangeMeters, double rangeRateMetersPerSec) { |
||||
this.rangeMeters = rangeMeters; |
||||
this.rangeRateMetersPerSec = rangeRateMetersPerSec; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,330 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.location.lbs.gnss.gps.pseudorange; |
||||
|
||||
/** |
||||
* Calculate the troposheric delay based on the ENGOS Tropospheric model. |
||||
* |
||||
* <p>The tropospheric delay is modeled as a combined effect of the delay experienced due to |
||||
* hyrostatic (dry) and wet components of the troposphere. Both delays experienced at zenith are |
||||
* scaled with a mapping function to get the delay at any specific elevation. |
||||
* |
||||
* <p>The tropospheric model algorithm of EGNOS model by Penna, N., A. Dodson and W. Chen (2001) |
||||
* (http://espace.library.curtin.edu.au/cgi-bin/espace.pdf?file=/2008/11/13/file_1/18917) is used
|
||||
* for calculating the zenith delays. In this model, the weather parameters are extracted using |
||||
* interpolation from lookup table derived from the US Standard Atmospheric Supplements, 1966. |
||||
* |
||||
* <p>A close form mapping function is built using Guo and Langley, 2003 |
||||
* (http://gauss2.gge.unb.ca/papers.pdf/iongpsgnss2003.guo.pdf) which is able to calculate accurate
|
||||
* mapping down to 2 degree elevations. |
||||
* |
||||
* <p>Sources: |
||||
* <p>http://espace.library.curtin.edu.au/cgi-bin/espace.pdf?file=/2008/11/13/file_1/18917
|
||||
* <p>- http://www.academia.edu/3512180/Assessment_of_UNB3M_neutral
|
||||
* _atmosphere_model_and_EGNOS_model_for_near-equatorial-tropospheric_delay_correction |
||||
* <p>- http://gauss.gge.unb.ca/papers.pdf/ion52am.collins.pdf
|
||||
* <p>- http://www.navipedia.net/index.php/Tropospheric_Delay#cite_ref-3
|
||||
* <p>Hydrostatic and non-hydrostatic mapping functions are obtained from: |
||||
* http://gauss2.gge.unb.ca/papers.pdf/iongpsgnss2003.guo.pdf
|
||||
* |
||||
*/ |
||||
public class TroposphericModelEgnos { |
||||
// parameters of the EGNOS models
|
||||
private static final int INDEX_15_DEGREES = 0; |
||||
private static final int INDEX_75_DEGREES = 4; |
||||
private static final int LATITUDE_15_DEGREES = 15; |
||||
private static final int LATITUDE_75_DEGREES = 75; |
||||
// Lookup Average parameters
|
||||
// Troposphere average presssure mbar
|
||||
private static final double[] latDegreeToPressureMbarAvgMap = |
||||
{1013.25, 1017.25, 1015.75, 1011.75, 1013.0}; |
||||
// Troposphere average temperature Kelvin
|
||||
private static final double[] latDegreeToTempKelvinAvgMap = |
||||
{299.65, 294.15, 283.15, 272.15, 263.65}; |
||||
// Troposphere average wator vapor pressure
|
||||
private static final double[] latDegreeToWVPressureMbarAvgMap = {26.31, 21.79, 11.66, 6.78, 4.11}; |
||||
// Troposphere average temperature lapse rate K/m
|
||||
private static final double[] latDegreeToBetaAvgMapKPM = |
||||
{6.30e-3, 6.05e-3, 5.58e-3, 5.39e-3, 4.53e-3}; |
||||
// Troposphere average water vapor lapse rate (dimensionless)
|
||||
private static final double[] latDegreeToLampdaAvgMap = {2.77, 3.15, 2.57, 1.81, 1.55}; |
||||
|
||||
// Lookup Amplitude parameters
|
||||
// Troposphere amplitude presssure mbar
|
||||
private static final double[] latDegreeToPressureMbarAmpMap = {0.0, -3.75, -2.25, -1.75, -0.5}; |
||||
// Troposphere amplitude temperature Kelvin
|
||||
private static final double[] latDegreeToTempKelvinAmpMap = {0.0, 7.0, 11.0, 15.0, 14.5}; |
||||
// Troposphere amplitude wator vapor pressure
|
||||
private static final double[] latDegreeToWVPressureMbarAmpMap = {0.0, 8.85, 7.24, 5.36, 3.39}; |
||||
// Troposphere amplitude temperature lapse rate K/m
|
||||
private static final double[] latDegreeToBetaAmpMapKPM = |
||||
{0.0, 0.25e-3, 0.32e-3, 0.81e-3, 0.62e-3}; |
||||
// Troposphere amplitude water vapor lapse rate (dimensionless)
|
||||
private static final double[] latDegreeToLampdaAmpMap = {0.0, 0.33, 0.46, 0.74, 0.30}; |
||||
// Zenith delay dry constant K/mbar
|
||||
private static final double K1 = 77.604; |
||||
// Zenith delay wet constant K^2/mbar
|
||||
private static final double K2 = 382000.0; |
||||
// gas constant for dry air J/kg/K
|
||||
private static final double RD = 287.054; |
||||
// Acceleration of gravity at the atmospheric column centroid m/s^-2
|
||||
private static final double GM = 9.784; |
||||
// Gravity m/s^2
|
||||
private static final double GRAVITY_MPS2 = 9.80665; |
||||
|
||||
private static final double MINIMUM_INTERPOLATION_THRESHOLD = 1e-25; |
||||
private static final double B_HYDROSTATIC = 0.0035716; |
||||
private static final double C_HYDROSTATIC = 0.082456; |
||||
private static final double B_NON_HYDROSTATIC = 0.0018576; |
||||
private static final double C_NON_HYDROSTATIC = 0.062741; |
||||
private static final double SOUTHERN_HEMISPHERE_DMIN = 211.0; |
||||
private static final double NORTHERN_HEMISPHERE_DMIN = 28.0; |
||||
// Days recalling that every fourth year is a leap year and has an extra day - February 29th
|
||||
private static final double DAYS_PER_YEAR = 365.25; |
||||
|
||||
/** |
||||
* Computes the tropospheric correction in meters given the satellite elevation in radians, the |
||||
* user latitude in radians, the user Orthometric height above sea level in meters and the day of |
||||
* the year. |
||||
* |
||||
* <p>Dry and wet delay zenith delay components are calculated and then scaled with the mapping |
||||
* function at the given satellite elevation. |
||||
* |
||||
*/ |
||||
public static double calculateTropoCorrectionMeters(double satElevationRadians, |
||||
double userLatitudeRadian, double heightMetersAboveSeaLevel, int dayOfYear1To366) { |
||||
DryAndWetMappingValues dryAndWetMappingValues = |
||||
computeDryAndWetMappingValuesUsingUNBabcMappingFunction(satElevationRadians, |
||||
userLatitudeRadian, heightMetersAboveSeaLevel); |
||||
DryAndWetZenithDelays dryAndWetZenithDelays = calculateZenithDryAndWetDelaysSec |
||||
(userLatitudeRadian, heightMetersAboveSeaLevel, dayOfYear1To366); |
||||
|
||||
double drydelaySeconds = |
||||
dryAndWetZenithDelays.dryZenithDelaySec * dryAndWetMappingValues.dryMappingValue; |
||||
double wetdelaySeconds = |
||||
dryAndWetZenithDelays.wetZenithDelaySec * dryAndWetMappingValues.wetMappingValue; |
||||
return drydelaySeconds + wetdelaySeconds; |
||||
} |
||||
|
||||
/** |
||||
* Computes the dry and wet mapping values based on the University of Brunswick UNBabc model. The |
||||
* mapping function inputs are satellite elevation in radians, user latitude in radians and user |
||||
* orthometric height above sea level in meters. The function returns |
||||
* {@code DryAndWetMappingValues} containing dry and wet mapping values. |
||||
* |
||||
* <p>From the many dry and wet mapping functions of components of the troposphere, the method |
||||
* from the University of Brunswick in Canada was selected due to its reasonable computation time |
||||
* and accuracy with satellites as low as 2 degrees elevation. |
||||
* <p>Source: http://gauss2.gge.unb.ca/papers.pdf/iongpsgnss2003.guo.pdf
|
||||
*/ |
||||
private static DryAndWetMappingValues computeDryAndWetMappingValuesUsingUNBabcMappingFunction( |
||||
double satElevationRadians, double userLatitudeRadians, double heightMetersAboveSeaLevel) { |
||||
|
||||
if (satElevationRadians > Math.PI / 2.0) { |
||||
satElevationRadians = Math.PI / 2.0; |
||||
} else if (satElevationRadians < 2.0 * Math.PI / 180.0) { |
||||
satElevationRadians = Math.toRadians(2.0); |
||||
} |
||||
|
||||
// dry components mapping parameters
|
||||
double aHidrostatic = (1.18972 - 0.026855 * heightMetersAboveSeaLevel / 1000.0 + 0.10664 |
||||
* Math.cos(userLatitudeRadians)) / 1000.0; |
||||
|
||||
|
||||
double numeratorDry = 1.0 + (aHidrostatic / (1.0 + (B_HYDROSTATIC / (1.0 + C_HYDROSTATIC)))); |
||||
double denominatorDry = Math.sin(satElevationRadians) + (aHidrostatic / ( |
||||
Math.sin(satElevationRadians) |
||||
+ (B_HYDROSTATIC / (Math.sin(satElevationRadians) + C_HYDROSTATIC)))); |
||||
|
||||
double drymap = numeratorDry / denominatorDry; |
||||
|
||||
// wet components mapping parameters
|
||||
double aNonHydrostatic = (0.61120 - 0.035348 * heightMetersAboveSeaLevel / 1000.0 - 0.01526 |
||||
* Math.cos(userLatitudeRadians)) / 1000.0; |
||||
|
||||
|
||||
double numeratorWet = |
||||
1.0 + (aNonHydrostatic / (1.0 + (B_NON_HYDROSTATIC / (1.0 + C_NON_HYDROSTATIC)))); |
||||
double denominatorWet = Math.sin(satElevationRadians) + (aNonHydrostatic / ( |
||||
Math.sin(satElevationRadians) |
||||
+ (B_NON_HYDROSTATIC / (Math.sin(satElevationRadians) + C_NON_HYDROSTATIC)))); |
||||
|
||||
double wetmap = numeratorWet / denominatorWet; |
||||
return new DryAndWetMappingValues(drymap, wetmap); |
||||
} |
||||
|
||||
/** |
||||
* Computes the combined effect of the delay at zenith experienced due to hyrostatic (dry) and wet |
||||
* components of the troposphere. The function inputs are the user latitude in radians, user |
||||
* orthometric height above sea level in meters and the day of the year (1-366). The function |
||||
* returns a {@code DryAndWetZenithDelays} containing dry and wet delays at zenith. |
||||
* |
||||
* <p>EGNOS Tropospheric model by Penna et al. (2001) is used in this case. |
||||
* (http://espace.library.curtin.edu.au/cgi-bin/espace.pdf?file=/2008/11/13/file_1/18917)
|
||||
* |
||||
*/ |
||||
private static DryAndWetZenithDelays calculateZenithDryAndWetDelaysSec(double userLatitudeRadians, |
||||
double heightMetersAboveSeaLevel, int dayOfyear1To366) { |
||||
// interpolated meteorological values
|
||||
double pressureMbar; |
||||
double tempKelvin; |
||||
double waterVaporPressureMbar; |
||||
// temperature lapse rate, [K/m]
|
||||
double beta; |
||||
// water vapor lapse rate, dimensionless
|
||||
double lambda; |
||||
|
||||
double absLatitudeDeg = Math.toDegrees(Math.abs(userLatitudeRadians)); |
||||
// day of year min constant
|
||||
double dmin; |
||||
if (userLatitudeRadians < 0) { |
||||
dmin = SOUTHERN_HEMISPHERE_DMIN; |
||||
} else { |
||||
dmin = NORTHERN_HEMISPHERE_DMIN; |
||||
|
||||
} |
||||
double amplitudeScalefactor = Math.cos((2 * Math.PI * (dayOfyear1To366 - dmin)) |
||||
/ DAYS_PER_YEAR); |
||||
|
||||
if (absLatitudeDeg <= LATITUDE_15_DEGREES) { |
||||
pressureMbar = latDegreeToPressureMbarAvgMap[INDEX_15_DEGREES] |
||||
- latDegreeToPressureMbarAmpMap[INDEX_15_DEGREES] * amplitudeScalefactor; |
||||
tempKelvin = latDegreeToTempKelvinAvgMap[INDEX_15_DEGREES] |
||||
- latDegreeToTempKelvinAmpMap[INDEX_15_DEGREES] * amplitudeScalefactor; |
||||
waterVaporPressureMbar = latDegreeToWVPressureMbarAvgMap[INDEX_15_DEGREES] |
||||
- latDegreeToWVPressureMbarAmpMap[INDEX_15_DEGREES] * amplitudeScalefactor; |
||||
beta = latDegreeToBetaAvgMapKPM[INDEX_15_DEGREES] - latDegreeToBetaAmpMapKPM[INDEX_15_DEGREES] |
||||
* amplitudeScalefactor; |
||||
lambda = latDegreeToLampdaAmpMap[INDEX_15_DEGREES] - latDegreeToLampdaAmpMap[INDEX_15_DEGREES] |
||||
* amplitudeScalefactor; |
||||
} else if (absLatitudeDeg > LATITUDE_15_DEGREES && absLatitudeDeg < LATITUDE_75_DEGREES) { |
||||
int key = (int) (absLatitudeDeg / LATITUDE_15_DEGREES); |
||||
|
||||
double averagePressureMbar = interpolate(key * LATITUDE_15_DEGREES, |
||||
latDegreeToPressureMbarAvgMap[key - 1], (key + 1) * LATITUDE_15_DEGREES, |
||||
latDegreeToPressureMbarAvgMap[key], absLatitudeDeg); |
||||
double amplitudePressureMbar = interpolate(key * LATITUDE_15_DEGREES, |
||||
latDegreeToPressureMbarAmpMap[key - 1], (key + 1) * LATITUDE_15_DEGREES, |
||||
latDegreeToPressureMbarAmpMap[key], absLatitudeDeg); |
||||
pressureMbar = averagePressureMbar - amplitudePressureMbar * amplitudeScalefactor; |
||||
|
||||
double averageTempKelvin = interpolate(key * LATITUDE_15_DEGREES, |
||||
latDegreeToTempKelvinAvgMap[key - 1], (key + 1) * LATITUDE_15_DEGREES, |
||||
latDegreeToTempKelvinAvgMap[key], absLatitudeDeg); |
||||
double amplitudeTempKelvin = interpolate(key * LATITUDE_15_DEGREES, |
||||
latDegreeToTempKelvinAmpMap[key - 1], (key + 1) * LATITUDE_15_DEGREES, |
||||
latDegreeToTempKelvinAmpMap[key], absLatitudeDeg); |
||||
tempKelvin = averageTempKelvin - amplitudeTempKelvin * amplitudeScalefactor; |
||||
|
||||
double averageWaterVaporPressureMbar = interpolate(key * LATITUDE_15_DEGREES, |
||||
latDegreeToWVPressureMbarAvgMap[key - 1], (key + 1) * LATITUDE_15_DEGREES, |
||||
latDegreeToWVPressureMbarAvgMap[key], absLatitudeDeg); |
||||
double amplitudeWaterVaporPressureMbar = interpolate(key * LATITUDE_15_DEGREES, |
||||
latDegreeToWVPressureMbarAmpMap[key - 1], (key + 1) * LATITUDE_15_DEGREES, |
||||
latDegreeToWVPressureMbarAmpMap[key], absLatitudeDeg); |
||||
waterVaporPressureMbar = |
||||
averageWaterVaporPressureMbar - amplitudeWaterVaporPressureMbar * amplitudeScalefactor; |
||||
|
||||
double averageBeta = interpolate(key * LATITUDE_15_DEGREES, latDegreeToBetaAvgMapKPM[key - 1], |
||||
(key + 1) * LATITUDE_15_DEGREES, latDegreeToBetaAvgMapKPM[key], absLatitudeDeg); |
||||
double amplitudeBeta = interpolate(key * LATITUDE_15_DEGREES, |
||||
latDegreeToBetaAmpMapKPM[key - 1], (key + 1) * LATITUDE_15_DEGREES, |
||||
latDegreeToBetaAmpMapKPM[key], absLatitudeDeg); |
||||
beta = averageBeta - amplitudeBeta * amplitudeScalefactor; |
||||
|
||||
double averageLambda = interpolate(key * LATITUDE_15_DEGREES, |
||||
latDegreeToLampdaAvgMap[key - 1], (key + 1) * LATITUDE_15_DEGREES, |
||||
latDegreeToLampdaAvgMap[key], absLatitudeDeg); |
||||
double amplitudeLambda = interpolate(key * LATITUDE_15_DEGREES, |
||||
latDegreeToLampdaAmpMap[key - 1], (key + 1) * LATITUDE_15_DEGREES, |
||||
latDegreeToLampdaAmpMap[key], absLatitudeDeg); |
||||
lambda = averageLambda - amplitudeLambda * amplitudeScalefactor; |
||||
} else { |
||||
pressureMbar = latDegreeToPressureMbarAvgMap[INDEX_75_DEGREES] |
||||
- latDegreeToPressureMbarAmpMap[INDEX_75_DEGREES] * amplitudeScalefactor; |
||||
tempKelvin = latDegreeToTempKelvinAvgMap[INDEX_75_DEGREES] |
||||
- latDegreeToTempKelvinAmpMap[INDEX_75_DEGREES] * amplitudeScalefactor; |
||||
waterVaporPressureMbar = latDegreeToWVPressureMbarAvgMap[INDEX_75_DEGREES] |
||||
- latDegreeToWVPressureMbarAmpMap[INDEX_75_DEGREES] * amplitudeScalefactor; |
||||
beta = latDegreeToBetaAvgMapKPM[INDEX_75_DEGREES] - latDegreeToBetaAmpMapKPM[INDEX_75_DEGREES] |
||||
* amplitudeScalefactor; |
||||
lambda = latDegreeToLampdaAmpMap[INDEX_75_DEGREES] - latDegreeToLampdaAmpMap[INDEX_75_DEGREES] |
||||
* amplitudeScalefactor; |
||||
} |
||||
|
||||
double zenithDryDelayAtSeaLevelSeconds = (1.0e-6 * K1 * RD * pressureMbar) / GM; |
||||
double zenithWetDelayAtSeaLevelSeconds = (((1.0e-6 * K2 * RD) |
||||
/ (GM * (lambda + 1.0) - beta * RD)) * (waterVaporPressureMbar / tempKelvin)); |
||||
double commonBase = 1.0 - ((beta * heightMetersAboveSeaLevel) / tempKelvin); |
||||
|
||||
double powerDry = (GRAVITY_MPS2 / (RD * beta)); |
||||
double powerWet = (((lambda + 1.0) * GRAVITY_MPS2) / (RD * beta)) - 1.0; |
||||
double zenithDryDelaySeconds = zenithDryDelayAtSeaLevelSeconds * Math.pow(commonBase, powerDry); |
||||
double zenithWetDelaySeconds = zenithWetDelayAtSeaLevelSeconds * Math.pow(commonBase, powerWet); |
||||
return new DryAndWetZenithDelays(zenithDryDelaySeconds, zenithWetDelaySeconds); |
||||
} |
||||
|
||||
/** |
||||
* Interpolates linearly given two points (point1X, point1Y) and (point2X, point2Y). Given the |
||||
* desired value of x (xInterpolated), an interpolated value of y shall be computed and returned. |
||||
*/ |
||||
private static double interpolate(double point1X, double point1Y, double point2X, double point2Y, |
||||
double xOutput) { |
||||
// Check that xOutput is between the two interpolation points.
|
||||
if ((point1X < point2X && (xOutput < point1X || xOutput > point2X)) |
||||
|| (point2X < point1X && (xOutput < point2X || xOutput > point1X))) { |
||||
throw new IllegalArgumentException("Interpolated value is outside the interpolated region"); |
||||
} |
||||
double deltaX = point2X - point1X; |
||||
double yOutput; |
||||
|
||||
if (Math.abs(deltaX) > MINIMUM_INTERPOLATION_THRESHOLD) { |
||||
yOutput = point1Y + (xOutput - point1X) / deltaX * (point2Y - point1Y); |
||||
} else { |
||||
yOutput = point1Y; |
||||
} |
||||
return yOutput; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* A class containing dry and wet mapping values |
||||
*/ |
||||
private static class DryAndWetMappingValues { |
||||
public double dryMappingValue; |
||||
public double wetMappingValue; |
||||
|
||||
public DryAndWetMappingValues(double dryMappingValue, double wetMappingValue) { |
||||
this.dryMappingValue = dryMappingValue; |
||||
this.wetMappingValue = wetMappingValue; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* A class containing dry and wet delays in seconds experienced at zenith |
||||
*/ |
||||
private static class DryAndWetZenithDelays { |
||||
public double dryZenithDelaySec; |
||||
public double wetZenithDelaySec; |
||||
|
||||
public DryAndWetZenithDelays(double dryZenithDelay, double wetZenithDelay) { |
||||
this.dryZenithDelaySec = dryZenithDelay; |
||||
this.wetZenithDelaySec = wetZenithDelay; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,989 @@
|
||||
/* |
||||
* Copyright (C) 2017 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.google.location.lbs.gnss.gps.pseudorange; |
||||
|
||||
import com.google.common.annotations.VisibleForTesting; |
||||
import com.google.common.base.Preconditions; |
||||
import com.google.common.collect.Lists; |
||||
import com.google.location.lbs.gnss.gps.pseudorange.Ecef2LlaConverter.GeodeticLlaValues; |
||||
import com.google.location.lbs.gnss.gps.pseudorange.EcefToTopocentricConverter.TopocentricAEDValues; |
||||
import com.google.location.lbs.gnss.gps.pseudorange.SatellitePositionCalculator.PositionAndVelocity; |
||||
import android.location.cts.nano.Ephemeris.GpsEphemerisProto; |
||||
import android.location.cts.nano.Ephemeris.GpsNavMessageProto; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import org.apache.commons.math3.linear.Array2DRowRealMatrix; |
||||
import org.apache.commons.math3.linear.LUDecomposition; |
||||
import org.apache.commons.math3.linear.QRDecomposition; |
||||
import org.apache.commons.math3.linear.RealMatrix; |
||||
|
||||
/** |
||||
* Computes an iterative least square receiver position solution given the pseudorange (meters) and |
||||
* accumulated delta range (meters) measurements, receiver time of week, week number and the |
||||
* navigation message. |
||||
*/ |
||||
class UserPositionVelocityWeightedLeastSquare { |
||||
private static final double SPEED_OF_LIGHT_MPS = 299792458.0; |
||||
private static final int SECONDS_IN_WEEK = 604800; |
||||
private static final double LEAST_SQUARE_TOLERANCE_METERS = 4.0e-8; |
||||
/** Position correction threshold below which atmospheric correction will be applied */ |
||||
private static final double ATMPOSPHERIC_CORRECTIONS_THRESHOLD_METERS = 1000.0; |
||||
private static final int MINIMUM_NUMER_OF_SATELLITES = 4; |
||||
private static final double RESIDUAL_TO_REPEAT_LEAST_SQUARE_METERS = 20.0; |
||||
private static final int MAXIMUM_NUMBER_OF_LEAST_SQUARE_ITERATIONS = 100; |
||||
/** GPS C/A code chip width Tc = 1 microseconds */ |
||||
private static final double GPS_CHIP_WIDTH_T_C_SEC = 1.0e-6; |
||||
/** Narrow correlator with spacing d = 0.1 chip */ |
||||
private static final double GPS_CORRELATOR_SPACING_IN_CHIPS = 0.1; |
||||
/** Average time of DLL correlator T of 20 milliseconds */ |
||||
private static final double GPS_DLL_AVERAGING_TIME_SEC = 20.0e-3; |
||||
/** Average signal travel time from GPS satellite and earth */ |
||||
private static final double AVERAGE_TRAVEL_TIME_SECONDS = 70.0e-3; |
||||
private static final double SECONDS_PER_NANO = 1.0e-9; |
||||
private static final double DOUBLE_ROUND_OFF_TOLERANCE = 0.0000000001; |
||||
|
||||
private final PseudorangeSmoother pseudorangeSmoother; |
||||
private double geoidHeightMeters; |
||||
private ElevationApiHelper elevationApiHelper; |
||||
private boolean calculateGeoidMeters = true; |
||||
private RealMatrix geometryMatrix; |
||||
private double[] truthLocationForCorrectedResidualComputationEcef = null; |
||||
|
||||
/** Constructor */ |
||||
public UserPositionVelocityWeightedLeastSquare(PseudorangeSmoother pseudorangeSmoother) { |
||||
this.pseudorangeSmoother = pseudorangeSmoother; |
||||
} |
||||
|
||||
/** Constructor with Google Elevation API Key */ |
||||
public UserPositionVelocityWeightedLeastSquare(PseudorangeSmoother pseudorangeSmoother, |
||||
String elevationApiKey){ |
||||
this.pseudorangeSmoother = pseudorangeSmoother; |
||||
this.elevationApiHelper = new ElevationApiHelper(elevationApiKey); |
||||
} |
||||
|
||||
/** |
||||
* Sets the reference ground truth for pseudornage residual correction calculation. If no ground |
||||
* truth is set, no corrected pesudorange residual will be calculated. |
||||
*/ |
||||
public void setTruthLocationForCorrectedResidualComputationEcef |
||||
(double[] groundTruthForResidualCorrectionEcef) { |
||||
this.truthLocationForCorrectedResidualComputationEcef = groundTruthForResidualCorrectionEcef; |
||||
} |
||||
|
||||
/** |
||||
* Least square solution to calculate the user position given the navigation message, pseudorange |
||||
* and accumulated delta range measurements. Also calculates user velocity non-iteratively from |
||||
* Least square position solution. |
||||
* |
||||
* <p>The method fills the user position and velocity in ECEF coordinates and receiver clock |
||||
* offset in meters and clock offset rate in meters per second. |
||||
* |
||||
* <p>One can choose between no smoothing, using the carrier phase measurements (accumulated delta |
||||
* range) or the doppler measurements (pseudorange rate) for smoothing the pseudorange. The |
||||
* smoothing is applied only if time has changed below a specific threshold since last invocation. |
||||
* |
||||
* <p>Source for least squares: |
||||
* |
||||
* <ul> |
||||
* <li>http://www.u-blox.com/images/downloads/Product_Docs/GPS_Compendium%28GPS-X-02007%29.pdf
|
||||
* page 81 - 85 |
||||
* <li>Parkinson, B.W., Spilker Jr., J.J.: ‘Global positioning system: theory and applications’ |
||||
* page 412 - 414 |
||||
* </ul> |
||||
* |
||||
* <p>Sources for smoothing pseudorange with carrier phase measurements: |
||||
* |
||||
* <ul> |
||||
* <li>Satellite Communications and Navigation Systems book, page 424, |
||||
* <li>Principles of GNSS, Inertial, and Multisensor Integrated Navigation Systems, page 388, |
||||
* 389. |
||||
* </ul> |
||||
* |
||||
* <p>The function does not modify the smoothed measurement list {@code |
||||
* immutableSmoothedSatellitesToReceiverMeasurements} |
||||
* |
||||
* @param navMessageProto parameters of the navigation message |
||||
* @param usefulSatellitesToReceiverMeasurements Map of useful satellite PRN to {@link |
||||
* GpsMeasurementWithRangeAndUncertainty} containing receiver measurements for computing the |
||||
* position solution. |
||||
* @param receiverGPSTowAtReceptionSeconds Receiver estimate of GPS time of week (seconds) |
||||
* @param receiverGPSWeek Receiver estimate of GPS week (0-1024+) |
||||
* @param dayOfYear1To366 The day of the year between 1 and 366 |
||||
* @param positionVelocitySolutionECEF Solution array of the following format: |
||||
* [0-2] xyz solution of user. |
||||
* [3] clock bias of user. |
||||
* [4-6] velocity of user. |
||||
* [7] clock bias rate of user. |
||||
* @param positionVelocityUncertaintyEnu Uncertainty of calculated position and velocity solution |
||||
* in meters and mps local ENU system. Array has the following format: |
||||
* [0-2] Enu uncertainty of position solution in meters |
||||
* [3-5] Enu uncertainty of velocity solution in meters per second. |
||||
* @param pseudorangeResidualMeters The pseudorange residual corrected by subtracting expected |
||||
* psudorange calculated with the use clock bias of the highest elevation satellites. |
||||
*/ |
||||
public void calculateUserPositionVelocityLeastSquare( |
||||
GpsNavMessageProto navMessageProto, |
||||
List<GpsMeasurementWithRangeAndUncertainty> usefulSatellitesToReceiverMeasurements, |
||||
double receiverGPSTowAtReceptionSeconds, |
||||
int receiverGPSWeek, |
||||
int dayOfYear1To366, |
||||
double[] positionVelocitySolutionECEF, |
||||
double[] positionVelocityUncertaintyEnu, |
||||
double[] pseudorangeResidualMeters) |
||||
throws Exception { |
||||
|
||||
// Use PseudorangeSmoother to smooth the pseudorange according to: Satellite Communications and
|
||||
// Navigation Systems book, page 424 and Principles of GNSS, Inertial, and Multisensor
|
||||
// Integrated Navigation Systems, page 388, 389.
|
||||
double[] deltaPositionMeters; |
||||
List<GpsMeasurementWithRangeAndUncertainty> immutableSmoothedSatellitesToReceiverMeasurements = |
||||
pseudorangeSmoother.updatePseudorangeSmoothingResult( |
||||
Collections.unmodifiableList(usefulSatellitesToReceiverMeasurements)); |
||||
List<GpsMeasurementWithRangeAndUncertainty> mutableSmoothedSatellitesToReceiverMeasurements = |
||||
Lists.newArrayList(immutableSmoothedSatellitesToReceiverMeasurements); |
||||
int numberOfUsefulSatellites = |
||||
getNumberOfUsefulSatellites(mutableSmoothedSatellitesToReceiverMeasurements); |
||||
// Least square position solution is supported only if 4 or more satellites visible
|
||||
Preconditions.checkArgument(numberOfUsefulSatellites >= MINIMUM_NUMER_OF_SATELLITES, |
||||
"At least 4 satellites have to be visible... Only 3D mode is supported..."); |
||||
boolean repeatLeastSquare = false; |
||||
SatellitesPositionPseudorangesResidualAndCovarianceMatrix satPosPseudorangeResidualAndWeight; |
||||
|
||||
boolean isFirstWLS = true; |
||||
|
||||
do { |
||||
// Calculate satellites' positions, measurement residuals per visible satellite and
|
||||
// weight matrix for the iterative least square
|
||||
boolean doAtmosphericCorrections = false; |
||||
satPosPseudorangeResidualAndWeight = |
||||
calculateSatPosAndPseudorangeResidual( |
||||
navMessageProto, |
||||
mutableSmoothedSatellitesToReceiverMeasurements, |
||||
receiverGPSTowAtReceptionSeconds, |
||||
receiverGPSWeek, |
||||
dayOfYear1To366, |
||||
positionVelocitySolutionECEF, |
||||
doAtmosphericCorrections); |
||||
|
||||
// Calculate the geometry matrix according to "Global Positioning System: Theory and
|
||||
// Applications", Parkinson and Spilker page 413
|
||||
RealMatrix covarianceMatrixM2 = |
||||
new Array2DRowRealMatrix(satPosPseudorangeResidualAndWeight.covarianceMatrixMetersSquare); |
||||
geometryMatrix = new Array2DRowRealMatrix(calculateGeometryMatrix( |
||||
satPosPseudorangeResidualAndWeight.satellitesPositionsMeters, |
||||
positionVelocitySolutionECEF)); |
||||
RealMatrix weightedGeometryMatrix; |
||||
RealMatrix weightMatrixMetersMinus2 = null; |
||||
// Apply weighted least square only if the covariance matrix is not singular (has a non-zero
|
||||
// determinant), otherwise apply ordinary least square. The reason is to ignore reported
|
||||
// signal to noise ratios by the receiver that can lead to such singularities
|
||||
LUDecomposition ludCovMatrixM2 = new LUDecomposition(covarianceMatrixM2); |
||||
double det = ludCovMatrixM2.getDeterminant(); |
||||
|
||||
if (det <= DOUBLE_ROUND_OFF_TOLERANCE) { |
||||
// Do not weight the geometry matrix if covariance matrix is singular.
|
||||
weightedGeometryMatrix = geometryMatrix; |
||||
} else { |
||||
weightMatrixMetersMinus2 = ludCovMatrixM2.getSolver().getInverse(); |
||||
RealMatrix hMatrix = |
||||
calculateHMatrix(weightMatrixMetersMinus2, geometryMatrix); |
||||
weightedGeometryMatrix = hMatrix.multiply(geometryMatrix.transpose()) |
||||
.multiply(weightMatrixMetersMinus2); |
||||
} |
||||
|
||||
// Equation 9 page 413 from "Global Positioning System: Theory and Applicaitons", Parkinson
|
||||
// and Spilker
|
||||
deltaPositionMeters = |
||||
GpsMathOperations.matrixByColVectMultiplication(weightedGeometryMatrix.getData(), |
||||
satPosPseudorangeResidualAndWeight.pseudorangeResidualsMeters); |
||||
|
||||
// Apply corrections to the position estimate
|
||||
positionVelocitySolutionECEF[0] += deltaPositionMeters[0]; |
||||
positionVelocitySolutionECEF[1] += deltaPositionMeters[1]; |
||||
positionVelocitySolutionECEF[2] += deltaPositionMeters[2]; |
||||
positionVelocitySolutionECEF[3] += deltaPositionMeters[3]; |
||||
// Iterate applying corrections to the position solution until correction is below threshold
|
||||
satPosPseudorangeResidualAndWeight = |
||||
applyWeightedLeastSquare( |
||||
navMessageProto, |
||||
mutableSmoothedSatellitesToReceiverMeasurements, |
||||
receiverGPSTowAtReceptionSeconds, |
||||
receiverGPSWeek, |
||||
dayOfYear1To366, |
||||
positionVelocitySolutionECEF, |
||||
deltaPositionMeters, |
||||
doAtmosphericCorrections, |
||||
satPosPseudorangeResidualAndWeight, |
||||
weightMatrixMetersMinus2); |
||||
|
||||
// We use the first WLS iteration results and correct them based on the ground truth position
|
||||
// and using a clock error computed from high elevation satellites. The first iteration is
|
||||
// used before satellite with high residuals being removed.
|
||||
if (isFirstWLS && truthLocationForCorrectedResidualComputationEcef != null) { |
||||
// Snapshot the information needed before high residual satellites are removed
|
||||
System.arraycopy( |
||||
ResidualCorrectionCalculator.calculateCorrectedResiduals( |
||||
satPosPseudorangeResidualAndWeight, |
||||
positionVelocitySolutionECEF.clone(), |
||||
truthLocationForCorrectedResidualComputationEcef), |
||||
0 /*source starting pos*/, |
||||
pseudorangeResidualMeters, |
||||
0 /*destination starting pos*/, |
||||
GpsNavigationMessageStore.MAX_NUMBER_OF_SATELLITES /*length of elements*/); |
||||
isFirstWLS = false; |
||||
} |
||||
repeatLeastSquare = false; |
||||
int satsWithResidualBelowThreshold = |
||||
satPosPseudorangeResidualAndWeight.pseudorangeResidualsMeters.length; |
||||
// remove satellites that have residuals above RESIDUAL_TO_REPEAT_LEAST_SQUARE_METERS as they
|
||||
// worsen the position solution accuracy. If any satellite is removed, repeat the least square
|
||||
repeatLeastSquare = |
||||
removeHighResidualSats( |
||||
mutableSmoothedSatellitesToReceiverMeasurements, |
||||
repeatLeastSquare, |
||||
satPosPseudorangeResidualAndWeight, |
||||
satsWithResidualBelowThreshold); |
||||
|
||||
} while (repeatLeastSquare); |
||||
calculateGeoidMeters = false; |
||||
|
||||
// The computed ECEF position will be used next to compute the user velocity.
|
||||
// we calculate and fill in the user velocity solutions based on following equation:
|
||||
// Weight Matrix * GeometryMatrix * User Velocity Vector
|
||||
// = Weight Matrix * deltaPseudoRangeRateWeightedMps
|
||||
// Reference: Pratap Misra and Per Enge
|
||||
// "Global Positioning System: Signals, Measurements, and Performance" Page 218.
|
||||
|
||||
// Get the number of satellite used in Geometry Matrix
|
||||
numberOfUsefulSatellites = geometryMatrix.getRowDimension(); |
||||
|
||||
RealMatrix rangeRateMps = new Array2DRowRealMatrix(numberOfUsefulSatellites, 1); |
||||
RealMatrix deltaPseudoRangeRateMps = |
||||
new Array2DRowRealMatrix(numberOfUsefulSatellites, 1); |
||||
RealMatrix pseudorangeRateWeight |
||||
= new Array2DRowRealMatrix(numberOfUsefulSatellites, numberOfUsefulSatellites); |
||||
|
||||
// Correct the receiver time of week with the estimated receiver clock bias
|
||||
receiverGPSTowAtReceptionSeconds = |
||||
receiverGPSTowAtReceptionSeconds - positionVelocitySolutionECEF[3] / SPEED_OF_LIGHT_MPS; |
||||
|
||||
int measurementCount = 0; |
||||
|
||||
// Calculate range rates
|
||||
for (int i = 0; i < GpsNavigationMessageStore.MAX_NUMBER_OF_SATELLITES; i++) { |
||||
if (mutableSmoothedSatellitesToReceiverMeasurements.get(i) != null) { |
||||
GpsEphemerisProto ephemeridesProto = getEphemerisForSatellite(navMessageProto, i + 1); |
||||
|
||||
double pseudorangeMeasurementMeters = |
||||
mutableSmoothedSatellitesToReceiverMeasurements.get(i).pseudorangeMeters; |
||||
GpsTimeOfWeekAndWeekNumber correctedTowAndWeek = |
||||
calculateCorrectedTransmitTowAndWeek(ephemeridesProto, receiverGPSTowAtReceptionSeconds, |
||||
receiverGPSWeek, pseudorangeMeasurementMeters); |
||||
|
||||
// Calculate satellite velocity
|
||||
PositionAndVelocity satPosECEFMetersVelocityMPS = SatellitePositionCalculator |
||||
.calculateSatellitePositionAndVelocityFromEphemeris( |
||||
ephemeridesProto, |
||||
correctedTowAndWeek.gpsTimeOfWeekSeconds, |
||||
correctedTowAndWeek.weekNumber, |
||||
positionVelocitySolutionECEF[0], |
||||
positionVelocitySolutionECEF[1], |
||||
positionVelocitySolutionECEF[2]); |
||||
|
||||
// Calculate satellite clock error rate
|
||||
double satelliteClockErrorRateMps = SatelliteClockCorrectionCalculator. |
||||
calculateSatClockCorrErrorRate( |
||||
ephemeridesProto, |
||||
correctedTowAndWeek.gpsTimeOfWeekSeconds, |
||||
correctedTowAndWeek.weekNumber); |
||||
|
||||
// Fill in range rates. range rate = satellite velocity (dot product) line-of-sight vector
|
||||
rangeRateMps.setEntry(measurementCount, 0, -1 * ( |
||||
satPosECEFMetersVelocityMPS.velocityXMetersPerSec |
||||
* geometryMatrix.getEntry(measurementCount, 0) |
||||
+ satPosECEFMetersVelocityMPS.velocityYMetersPerSec |
||||
* geometryMatrix.getEntry(measurementCount, 1) |
||||
+ satPosECEFMetersVelocityMPS.velocityZMetersPerSec |
||||
* geometryMatrix.getEntry(measurementCount, 2))); |
||||
|
||||
deltaPseudoRangeRateMps.setEntry(measurementCount, 0, |
||||
mutableSmoothedSatellitesToReceiverMeasurements.get(i).pseudorangeRateMps |
||||
- rangeRateMps.getEntry(measurementCount, 0) + satelliteClockErrorRateMps |
||||
- positionVelocitySolutionECEF[7]); |
||||
|
||||
// Calculate the velocity weight matrix by using 1 / square(Pseudorangerate Uncertainty)
|
||||
// along the diagonal
|
||||
pseudorangeRateWeight.setEntry(measurementCount, measurementCount, |
||||
1 / (mutableSmoothedSatellitesToReceiverMeasurements |
||||
.get(i).pseudorangeRateUncertaintyMps |
||||
* mutableSmoothedSatellitesToReceiverMeasurements |
||||
.get(i).pseudorangeRateUncertaintyMps)); |
||||
measurementCount++; |
||||
} |
||||
} |
||||
|
||||
RealMatrix weightedGeoMatrix = pseudorangeRateWeight.multiply(geometryMatrix); |
||||
RealMatrix deltaPseudoRangeRateWeightedMps = |
||||
pseudorangeRateWeight.multiply(deltaPseudoRangeRateMps); |
||||
QRDecomposition qrdWeightedGeoMatrix = new QRDecomposition(weightedGeoMatrix); |
||||
RealMatrix velocityMps |
||||
= qrdWeightedGeoMatrix.getSolver().solve(deltaPseudoRangeRateWeightedMps); |
||||
positionVelocitySolutionECEF[4] = velocityMps.getEntry(0, 0); |
||||
positionVelocitySolutionECEF[5] = velocityMps.getEntry(1, 0); |
||||
positionVelocitySolutionECEF[6] = velocityMps.getEntry(2, 0); |
||||
positionVelocitySolutionECEF[7] = velocityMps.getEntry(3, 0); |
||||
|
||||
RealMatrix pseudorangeWeight |
||||
= new LUDecomposition( |
||||
new Array2DRowRealMatrix(satPosPseudorangeResidualAndWeight.covarianceMatrixMetersSquare |
||||
) |
||||
).getSolver().getInverse(); |
||||
|
||||
// Calculate and store the uncertainties of position and velocity in local ENU system in meters
|
||||
// and meters per second.
|
||||
double[] pvUncertainty = |
||||
calculatePositionVelocityUncertaintyEnu(pseudorangeRateWeight, pseudorangeWeight, |
||||
positionVelocitySolutionECEF); |
||||
System.arraycopy(pvUncertainty, |
||||
0 /*source starting pos*/, |
||||
positionVelocityUncertaintyEnu, |
||||
0 /*destination starting pos*/, |
||||
6 /*length of elements*/); |
||||
} |
||||
|
||||
/** |
||||
* Calculates the position uncertainty in meters and the velocity uncertainty |
||||
* in meters per second solution in local ENU system. |
||||
* |
||||
* <p> Reference: Global Positioning System: Signals, Measurements, and Performance |
||||
* by Pratap Misra, Per Enge, Page 206 - 209. |
||||
* |
||||
* @param velocityWeightMatrix the velocity weight matrix |
||||
* @param positionWeightMatrix the position weight matrix |
||||
* @param positionVelocitySolution the position and velocity solution in ECEF |
||||
* @return an array containing the position and velocity uncertainties in ENU coordinate system. |
||||
* [0-2] Enu uncertainty of position solution in meters. |
||||
* [3-5] Enu uncertainty of velocity solution in meters per second. |
||||
*/ |
||||
public double[] calculatePositionVelocityUncertaintyEnu( |
||||
RealMatrix velocityWeightMatrix, RealMatrix positionWeightMatrix, |
||||
double[] positionVelocitySolution){ |
||||
|
||||
if (geometryMatrix == null){ |
||||
return null; |
||||
} |
||||
|
||||
RealMatrix velocityH = calculateHMatrix(velocityWeightMatrix, geometryMatrix); |
||||
RealMatrix positionH = calculateHMatrix(positionWeightMatrix, geometryMatrix); |
||||
|
||||
// Calculate the rotation Matrix to convert to local ENU system.
|
||||
RealMatrix rotationMatrix = new Array2DRowRealMatrix(4, 4); |
||||
GeodeticLlaValues llaValues = Ecef2LlaConverter.convertECEFToLLACloseForm |
||||
(positionVelocitySolution[0], positionVelocitySolution[1], positionVelocitySolution[2]); |
||||
rotationMatrix.setSubMatrix( |
||||
Ecef2EnuConverter.getRotationMatrix(llaValues.longitudeRadians, |
||||
llaValues.latitudeRadians).getData(), 0, 0); |
||||
rotationMatrix.setEntry(3, 3, 1); |
||||
|
||||
// Convert to local ENU by pre-multiply rotation matrix and multiply rotation matrix transposed
|
||||
velocityH = rotationMatrix.multiply(velocityH).multiply(rotationMatrix.transpose()); |
||||
positionH = rotationMatrix.multiply(positionH).multiply(rotationMatrix.transpose()); |
||||
|
||||
// Return the square root of diagonal entries
|
||||
return new double[] { |
||||
Math.sqrt(positionH.getEntry(0, 0)), Math.sqrt(positionH.getEntry(1, 1)), |
||||
Math.sqrt(positionH.getEntry(2, 2)), Math.sqrt(velocityH.getEntry(0, 0)), |
||||
Math.sqrt(velocityH.getEntry(1, 1)), Math.sqrt(velocityH.getEntry(2, 2))}; |
||||
} |
||||
|
||||
/** |
||||
* Calculates the measurement connection matrix H as a function of weightMatrix and |
||||
* geometryMatrix. |
||||
* |
||||
* <p> H = (geometryMatrixTransposed * Weight * geometryMatrix) ^ -1 |
||||
* |
||||
* <p> Reference: Global Positioning System: Signals, Measurements, and Performance, P207 |
||||
* @param weightMatrix Weights for computing H Matrix |
||||
* @return H Matrix |
||||
*/ |
||||
private RealMatrix calculateHMatrix |
||||
(RealMatrix weightMatrix, RealMatrix geometryMatrix){ |
||||
|
||||
RealMatrix tempH = geometryMatrix.transpose().multiply(weightMatrix).multiply(geometryMatrix); |
||||
return new LUDecomposition(tempH).getSolver().getInverse(); |
||||
} |
||||
|
||||
/** |
||||
* Applies weighted least square iterations and corrects to the position solution until correction |
||||
* is below threshold. An exception is thrown if the maximum number of iterations: |
||||
* {@value #MAXIMUM_NUMBER_OF_LEAST_SQUARE_ITERATIONS} is reached without convergence. |
||||
*/ |
||||
private SatellitesPositionPseudorangesResidualAndCovarianceMatrix applyWeightedLeastSquare( |
||||
GpsNavMessageProto navMessageProto, |
||||
List<GpsMeasurementWithRangeAndUncertainty> usefulSatellitesToReceiverMeasurements, |
||||
double receiverGPSTowAtReceptionSeconds, |
||||
int receiverGPSWeek, |
||||
int dayOfYear1To366, |
||||
double[] positionSolutionECEF, |
||||
double[] deltaPositionMeters, |
||||
boolean doAtmosphericCorrections, |
||||
SatellitesPositionPseudorangesResidualAndCovarianceMatrix satPosPseudorangeResidualAndWeight, |
||||
RealMatrix weightMatrixMetersMinus2) |
||||
throws Exception { |
||||
RealMatrix weightedGeometryMatrix; |
||||
int numberOfIterations = 0; |
||||
|
||||
while ((Math.abs(deltaPositionMeters[0]) + Math.abs(deltaPositionMeters[1]) |
||||
+ Math.abs(deltaPositionMeters[2])) >= LEAST_SQUARE_TOLERANCE_METERS) { |
||||
// Apply ionospheric and tropospheric corrections only if the applied correction to
|
||||
// position is below a specific threshold
|
||||
if ((Math.abs(deltaPositionMeters[0]) + Math.abs(deltaPositionMeters[1]) |
||||
+ Math.abs(deltaPositionMeters[2])) < ATMPOSPHERIC_CORRECTIONS_THRESHOLD_METERS) { |
||||
doAtmosphericCorrections = true; |
||||
} |
||||
// Calculate satellites' positions, measurement residual per visible satellite and
|
||||
// weight matrix for the iterative least square
|
||||
satPosPseudorangeResidualAndWeight = |
||||
calculateSatPosAndPseudorangeResidual( |
||||
navMessageProto, |
||||
usefulSatellitesToReceiverMeasurements, |
||||
receiverGPSTowAtReceptionSeconds, |
||||
receiverGPSWeek, |
||||
dayOfYear1To366, |
||||
positionSolutionECEF, |
||||
doAtmosphericCorrections); |
||||
|
||||
// Calculate the geometry matrix according to "Global Positioning System: Theory and
|
||||
// Applications", Parkinson and Spilker page 413
|
||||
geometryMatrix = new Array2DRowRealMatrix(calculateGeometryMatrix( |
||||
satPosPseudorangeResidualAndWeight.satellitesPositionsMeters, positionSolutionECEF)); |
||||
// Apply weighted least square only if the covariance matrix is
|
||||
// not singular (has a non-zero determinant), otherwise apply ordinary least square.
|
||||
// The reason is to ignore reported signal to noise ratios by the receiver that can
|
||||
// lead to such singularities
|
||||
if (weightMatrixMetersMinus2 == null) { |
||||
weightedGeometryMatrix = geometryMatrix; |
||||
} else { |
||||
RealMatrix hMatrix = |
||||
calculateHMatrix(weightMatrixMetersMinus2, geometryMatrix); |
||||
weightedGeometryMatrix = hMatrix.multiply(geometryMatrix.transpose()) |
||||
.multiply(weightMatrixMetersMinus2); |
||||
} |
||||
|
||||
// Equation 9 page 413 from "Global Positioning System: Theory and Applicaitons",
|
||||
// Parkinson and Spilker
|
||||
deltaPositionMeters = |
||||
GpsMathOperations.matrixByColVectMultiplication( |
||||
weightedGeometryMatrix.getData(), |
||||
satPosPseudorangeResidualAndWeight.pseudorangeResidualsMeters); |
||||
|
||||
// Apply corrections to the position estimate
|
||||
positionSolutionECEF[0] += deltaPositionMeters[0]; |
||||
positionSolutionECEF[1] += deltaPositionMeters[1]; |
||||
positionSolutionECEF[2] += deltaPositionMeters[2]; |
||||
positionSolutionECEF[3] += deltaPositionMeters[3]; |
||||
numberOfIterations++; |
||||
Preconditions.checkArgument(numberOfIterations <= MAXIMUM_NUMBER_OF_LEAST_SQUARE_ITERATIONS, |
||||
"Maximum number of least square iterations reached without convergance..."); |
||||
} |
||||
return satPosPseudorangeResidualAndWeight; |
||||
} |
||||
|
||||
/** |
||||
* Removes satellites that have residuals above {@value #RESIDUAL_TO_REPEAT_LEAST_SQUARE_METERS} |
||||
* from the {@code usefulSatellitesToReceiverMeasurements} list. Returns true if any satellite is |
||||
* removed. |
||||
*/ |
||||
private boolean removeHighResidualSats( |
||||
List<GpsMeasurementWithRangeAndUncertainty> usefulSatellitesToReceiverMeasurements, |
||||
boolean repeatLeastSquare, |
||||
SatellitesPositionPseudorangesResidualAndCovarianceMatrix satPosPseudorangeResidualAndWeight, |
||||
int satsWithResidualBelowThreshold) { |
||||
|
||||
for (int i = 0; i < satPosPseudorangeResidualAndWeight.pseudorangeResidualsMeters.length; i++) { |
||||
if (satsWithResidualBelowThreshold > MINIMUM_NUMER_OF_SATELLITES) { |
||||
if (Math.abs(satPosPseudorangeResidualAndWeight.pseudorangeResidualsMeters[i]) |
||||
> RESIDUAL_TO_REPEAT_LEAST_SQUARE_METERS) { |
||||
int prn = satPosPseudorangeResidualAndWeight.satellitePRNs[i]; |
||||
usefulSatellitesToReceiverMeasurements.set(prn - 1, null); |
||||
satsWithResidualBelowThreshold--; |
||||
repeatLeastSquare = true; |
||||
} |
||||
} |
||||
} |
||||
return repeatLeastSquare; |
||||
} |
||||
|
||||
/** |
||||
* Calculates position of all visible satellites and pseudorange measurement residual |
||||
* (difference of measured to predicted pseudoranges) needed for the least square computation. The |
||||
* result is stored in an instance of {@link |
||||
* SatellitesPositionPseudorangesResidualAndCovarianceMatrix} |
||||
* |
||||
* @param navMeassageProto parameters of the navigation message |
||||
* @param usefulSatellitesToReceiverMeasurements Map of useful satellite PRN to {@link |
||||
* GpsMeasurementWithRangeAndUncertainty} containing receiver measurements for computing the |
||||
* position solution |
||||
* @param receiverGPSTowAtReceptionSeconds Receiver estimate of GPS time of week (seconds) |
||||
* @param receiverGpsWeek Receiver estimate of GPS week (0-1024+) |
||||
* @param dayOfYear1To366 The day of the year between 1 and 366 |
||||
* @param userPositionECEFMeters receiver ECEF position in meters |
||||
* @param doAtmosphericCorrections boolean indicating if atmospheric range corrections should be |
||||
* applied |
||||
* @return SatellitesPositionPseudorangesResidualAndCovarianceMatrix Object containing satellite |
||||
* prns, satellite positions in ECEF, pseudorange residuals and covariance matrix. |
||||
*/ |
||||
public SatellitesPositionPseudorangesResidualAndCovarianceMatrix |
||||
calculateSatPosAndPseudorangeResidual( |
||||
GpsNavMessageProto navMeassageProto, |
||||
List<GpsMeasurementWithRangeAndUncertainty> usefulSatellitesToReceiverMeasurements, |
||||
double receiverGPSTowAtReceptionSeconds, |
||||
int receiverGpsWeek, |
||||
int dayOfYear1To366, |
||||
double[] userPositionECEFMeters, |
||||
boolean doAtmosphericCorrections) |
||||
throws Exception { |
||||
int numberOfUsefulSatellites = |
||||
getNumberOfUsefulSatellites(usefulSatellitesToReceiverMeasurements); |
||||
// deltaPseudorange is the pseudorange measurement residual
|
||||
double[] deltaPseudorangesMeters = new double[numberOfUsefulSatellites]; |
||||
double[][] satellitesPositionsECEFMeters = new double[numberOfUsefulSatellites][3]; |
||||
|
||||
// satellite PRNs
|
||||
int[] satellitePRNs = new int[numberOfUsefulSatellites]; |
||||
|
||||
// Ionospheric model parameters
|
||||
double[] alpha = |
||||
{navMeassageProto.iono.alpha[0], navMeassageProto.iono.alpha[1], |
||||
navMeassageProto.iono.alpha[2], navMeassageProto.iono.alpha[3]}; |
||||
double[] beta = {navMeassageProto.iono.beta[0], navMeassageProto.iono.beta[1], |
||||
navMeassageProto.iono.beta[2], navMeassageProto.iono.beta[3]}; |
||||
// Weight matrix for the weighted least square
|
||||
RealMatrix covarianceMatrixMetersSquare = |
||||
new Array2DRowRealMatrix(numberOfUsefulSatellites, numberOfUsefulSatellites); |
||||
calculateSatPosAndResiduals( |
||||
navMeassageProto, |
||||
usefulSatellitesToReceiverMeasurements, |
||||
receiverGPSTowAtReceptionSeconds, |
||||
receiverGpsWeek, |
||||
dayOfYear1To366, |
||||
userPositionECEFMeters, |
||||
doAtmosphericCorrections, |
||||
deltaPseudorangesMeters, |
||||
satellitesPositionsECEFMeters, |
||||
satellitePRNs, |
||||
alpha, |
||||
beta, |
||||
covarianceMatrixMetersSquare); |
||||
|
||||
return new SatellitesPositionPseudorangesResidualAndCovarianceMatrix(satellitePRNs, |
||||
satellitesPositionsECEFMeters, deltaPseudorangesMeters, |
||||
covarianceMatrixMetersSquare.getData()); |
||||
} |
||||
|
||||
/** |
||||
* Calculates and fill the position of all visible satellites: |
||||
* {@code satellitesPositionsECEFMeters}, pseudorange measurement residual (difference of |
||||
* measured to predicted pseudoranges): {@code deltaPseudorangesMeters} and covariance matrix from |
||||
* the weighted least square: {@code covarianceMatrixMetersSquare}. An array of the satellite PRNs |
||||
* {@code satellitePRNs} is as well filled. |
||||
*/ |
||||
private void calculateSatPosAndResiduals( |
||||
GpsNavMessageProto navMeassageProto, |
||||
List<GpsMeasurementWithRangeAndUncertainty> usefulSatellitesToReceiverMeasurements, |
||||
double receiverGPSTowAtReceptionSeconds, |
||||
int receiverGpsWeek, |
||||
int dayOfYear1To366, |
||||
double[] userPositionECEFMeters, |
||||
boolean doAtmosphericCorrections, |
||||
double[] deltaPseudorangesMeters, |
||||
double[][] satellitesPositionsECEFMeters, |
||||
int[] satellitePRNs, |
||||
double[] alpha, |
||||
double[] beta, |
||||
RealMatrix covarianceMatrixMetersSquare) |
||||
throws Exception { |
||||
// user position without the clock estimate
|
||||
double[] userPositionTempECEFMeters = |
||||
{userPositionECEFMeters[0], userPositionECEFMeters[1], userPositionECEFMeters[2]}; |
||||
int satsCounter = 0; |
||||
for (int i = 0; i < GpsNavigationMessageStore.MAX_NUMBER_OF_SATELLITES; i++) { |
||||
if (usefulSatellitesToReceiverMeasurements.get(i) != null) { |
||||
GpsEphemerisProto ephemeridesProto = getEphemerisForSatellite(navMeassageProto, i + 1); |
||||
// Correct the receiver time of week with the estimated receiver clock bias
|
||||
receiverGPSTowAtReceptionSeconds = |
||||
receiverGPSTowAtReceptionSeconds - userPositionECEFMeters[3] / SPEED_OF_LIGHT_MPS; |
||||
|
||||
double pseudorangeMeasurementMeters = |
||||
usefulSatellitesToReceiverMeasurements.get(i).pseudorangeMeters; |
||||
double pseudorangeUncertaintyMeters = |
||||
usefulSatellitesToReceiverMeasurements.get(i).pseudorangeUncertaintyMeters; |
||||
|
||||
// Assuming uncorrelated pseudorange measurements, the covariance matrix will be diagonal as
|
||||
// follows
|
||||
covarianceMatrixMetersSquare.setEntry(satsCounter, satsCounter, |
||||
pseudorangeUncertaintyMeters * pseudorangeUncertaintyMeters); |
||||
|
||||
// Calculate time of week at transmission time corrected with the satellite clock drift
|
||||
GpsTimeOfWeekAndWeekNumber correctedTowAndWeek = |
||||
calculateCorrectedTransmitTowAndWeek(ephemeridesProto, receiverGPSTowAtReceptionSeconds, |
||||
receiverGpsWeek, pseudorangeMeasurementMeters); |
||||
|
||||
// calculate satellite position and velocity
|
||||
PositionAndVelocity satPosECEFMetersVelocityMPS = SatellitePositionCalculator |
||||
.calculateSatellitePositionAndVelocityFromEphemeris(ephemeridesProto, |
||||
correctedTowAndWeek.gpsTimeOfWeekSeconds, correctedTowAndWeek.weekNumber, |
||||
userPositionECEFMeters[0], userPositionECEFMeters[1], userPositionECEFMeters[2]); |
||||
|
||||
satellitesPositionsECEFMeters[satsCounter][0] = satPosECEFMetersVelocityMPS.positionXMeters; |
||||
satellitesPositionsECEFMeters[satsCounter][1] = satPosECEFMetersVelocityMPS.positionYMeters; |
||||
satellitesPositionsECEFMeters[satsCounter][2] = satPosECEFMetersVelocityMPS.positionZMeters; |
||||
|
||||
// Calculate ionospheric and tropospheric corrections
|
||||
double ionosphericCorrectionMeters; |
||||
double troposphericCorrectionMeters; |
||||
if (doAtmosphericCorrections) { |
||||
ionosphericCorrectionMeters = |
||||
IonosphericModel.ionoKloboucharCorrectionSeconds( |
||||
userPositionTempECEFMeters, |
||||
satellitesPositionsECEFMeters[satsCounter], |
||||
correctedTowAndWeek.gpsTimeOfWeekSeconds, |
||||
alpha, |
||||
beta, |
||||
IonosphericModel.L1_FREQ_HZ) |
||||
* SPEED_OF_LIGHT_MPS; |
||||
|
||||
troposphericCorrectionMeters = |
||||
calculateTroposphericCorrectionMeters( |
||||
dayOfYear1To366, |
||||
satellitesPositionsECEFMeters, |
||||
userPositionTempECEFMeters, |
||||
satsCounter); |
||||
} else { |
||||
troposphericCorrectionMeters = 0.0; |
||||
ionosphericCorrectionMeters = 0.0; |
||||
} |
||||
double predictedPseudorangeMeters = |
||||
calculatePredictedPseudorange(userPositionECEFMeters, satellitesPositionsECEFMeters, |
||||
userPositionTempECEFMeters, satsCounter, ephemeridesProto, correctedTowAndWeek, |
||||
ionosphericCorrectionMeters, troposphericCorrectionMeters); |
||||
|
||||
// Pseudorange residual (difference of measured to predicted pseudoranges)
|
||||
deltaPseudorangesMeters[satsCounter] = |
||||
pseudorangeMeasurementMeters - predictedPseudorangeMeters; |
||||
|
||||
// Satellite PRNs
|
||||
satellitePRNs[satsCounter] = i + 1; |
||||
satsCounter++; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** Searches ephemerides list for the ephemeris associated with current satellite in process */ |
||||
private GpsEphemerisProto getEphemerisForSatellite(GpsNavMessageProto navMeassageProto, |
||||
int satPrn) { |
||||
List<GpsEphemerisProto> ephemeridesList |
||||
= new ArrayList<GpsEphemerisProto>(Arrays.asList(navMeassageProto.ephemerids)); |
||||
GpsEphemerisProto ephemeridesProto = null; |
||||
int ephemerisPrn = 0; |
||||
for (GpsEphemerisProto ephProtoFromList : ephemeridesList) { |
||||
ephemerisPrn = ephProtoFromList.prn; |
||||
if (ephemerisPrn == satPrn) { |
||||
ephemeridesProto = ephProtoFromList; |
||||
break; |
||||
} |
||||
} |
||||
return ephemeridesProto; |
||||
} |
||||
|
||||
/** Calculates predicted pseudorange in meters */ |
||||
private double calculatePredictedPseudorange( |
||||
double[] userPositionECEFMeters, |
||||
double[][] satellitesPositionsECEFMeters, |
||||
double[] userPositionNoClockECEFMeters, |
||||
int satsCounter, |
||||
GpsEphemerisProto ephemeridesProto, |
||||
GpsTimeOfWeekAndWeekNumber correctedTowAndWeek, |
||||
double ionosphericCorrectionMeters, |
||||
double troposphericCorrectionMeters) |
||||
throws Exception { |
||||
// Calcualte the satellite clock drift
|
||||
double satelliteClockCorrectionMeters = |
||||
SatelliteClockCorrectionCalculator.calculateSatClockCorrAndEccAnomAndTkIteratively( |
||||
ephemeridesProto, |
||||
correctedTowAndWeek.gpsTimeOfWeekSeconds, |
||||
correctedTowAndWeek.weekNumber) |
||||
.satelliteClockCorrectionMeters; |
||||
|
||||
double satelliteToUserDistanceMeters = |
||||
GpsMathOperations.vectorNorm(GpsMathOperations.subtractTwoVectors( |
||||
satellitesPositionsECEFMeters[satsCounter], userPositionNoClockECEFMeters)); |
||||
// Predicted pseudorange
|
||||
double predictedPseudorangeMeters = |
||||
satelliteToUserDistanceMeters - satelliteClockCorrectionMeters + ionosphericCorrectionMeters |
||||
+ troposphericCorrectionMeters + userPositionECEFMeters[3]; |
||||
return predictedPseudorangeMeters; |
||||
} |
||||
|
||||
/** Calculates the Gps tropospheric correction in meters */ |
||||
private double calculateTroposphericCorrectionMeters(int dayOfYear1To366, |
||||
double[][] satellitesPositionsECEFMeters, double[] userPositionTempECEFMeters, |
||||
int satsCounter) { |
||||
double troposphericCorrectionMeters; |
||||
TopocentricAEDValues elevationAzimuthDist = |
||||
EcefToTopocentricConverter.convertCartesianToTopocentericRadMeters( |
||||
userPositionTempECEFMeters, GpsMathOperations.subtractTwoVectors( |
||||
satellitesPositionsECEFMeters[satsCounter], userPositionTempECEFMeters)); |
||||
|
||||
GeodeticLlaValues lla = |
||||
Ecef2LlaConverter.convertECEFToLLACloseForm(userPositionTempECEFMeters[0], |
||||
userPositionTempECEFMeters[1], userPositionTempECEFMeters[2]); |
||||
|
||||
// Geoid of the area where the receiver is located is calculated once and used for the
|
||||
// rest of the dataset as it change very slowly over wide area. This to save the delay
|
||||
// associated with accessing Google Elevation API. We assume this very first iteration of WLS
|
||||
// will compute the correct altitude above the ellipsoid of the ground at the latitude and
|
||||
// longitude
|
||||
if (calculateGeoidMeters) { |
||||
double elevationAboveSeaLevelMeters = 0; |
||||
if (elevationApiHelper == null){ |
||||
System.out.println("No Google API key is set. Elevation above sea level is set to " |
||||
+ "default 0 meters. This may cause inaccuracy in tropospheric correction."); |
||||
} else { |
||||
try { |
||||
elevationAboveSeaLevelMeters = elevationApiHelper |
||||
.getElevationAboveSeaLevelMeters( |
||||
Math.toDegrees(lla.latitudeRadians), Math.toDegrees(lla.longitudeRadians) |
||||
); |
||||
} catch (Exception e){ |
||||
e.printStackTrace(); |
||||
System.out.println("Error when getting elevation from Google Server. " |
||||
+ "Could be wrong Api key or network error. Elevation above sea level is set to " |
||||
+ "default 0 meters. This may cause inaccuracy in tropospheric correction."); |
||||
} |
||||
} |
||||
|
||||
geoidHeightMeters = ElevationApiHelper.calculateGeoidHeightMeters( |
||||
lla.altitudeMeters, |
||||
elevationAboveSeaLevelMeters |
||||
); |
||||
troposphericCorrectionMeters = TroposphericModelEgnos.calculateTropoCorrectionMeters( |
||||
elevationAzimuthDist.elevationRadians, lla.latitudeRadians, elevationAboveSeaLevelMeters, |
||||
dayOfYear1To366); |
||||
} else { |
||||
troposphericCorrectionMeters = TroposphericModelEgnos.calculateTropoCorrectionMeters( |
||||
elevationAzimuthDist.elevationRadians, lla.latitudeRadians, |
||||
lla.altitudeMeters - geoidHeightMeters, dayOfYear1To366); |
||||
} |
||||
return troposphericCorrectionMeters; |
||||
} |
||||
|
||||
/** |
||||
* Gets the number of useful satellites from a list of |
||||
* {@link GpsMeasurementWithRangeAndUncertainty}. |
||||
*/ |
||||
private int getNumberOfUsefulSatellites( |
||||
List<GpsMeasurementWithRangeAndUncertainty> usefulSatellitesToReceiverMeasurements) { |
||||
// calculate the number of useful satellites
|
||||
int numberOfUsefulSatellites = 0; |
||||
for (int i = 0; i < usefulSatellitesToReceiverMeasurements.size(); i++) { |
||||
if (usefulSatellitesToReceiverMeasurements.get(i) != null) { |
||||
numberOfUsefulSatellites++; |
||||
} |
||||
} |
||||
return numberOfUsefulSatellites; |
||||
} |
||||
|
||||
/** |
||||
* Computes the GPS time of week at the time of transmission and as well the corrected GPS week |
||||
* taking into consideration week rollover. The returned GPS time of week is corrected by the |
||||
* computed satellite clock drift. The result is stored in an instance of |
||||
* {@link GpsTimeOfWeekAndWeekNumber} |
||||
* |
||||
* @param ephemerisProto parameters of the navigation message |
||||
* @param receiverGpsTowAtReceptionSeconds Receiver estimate of GPS time of week when signal was |
||||
* received (seconds) |
||||
* @param receiverGpsWeek Receiver estimate of GPS week (0-1024+) |
||||
* @param pseudorangeMeters Measured pseudorange in meters |
||||
* @return GpsTimeOfWeekAndWeekNumber Object containing Gps time of week and week number. |
||||
*/ |
||||
private static GpsTimeOfWeekAndWeekNumber calculateCorrectedTransmitTowAndWeek( |
||||
GpsEphemerisProto ephemerisProto, double receiverGpsTowAtReceptionSeconds, |
||||
int receiverGpsWeek, double pseudorangeMeters) throws Exception { |
||||
// GPS time of week at time of transmission: Gps time corrected for transit time (page 98 ICD
|
||||
// GPS 200)
|
||||
double receiverGpsTowAtTimeOfTransmission = |
||||
receiverGpsTowAtReceptionSeconds - pseudorangeMeters / SPEED_OF_LIGHT_MPS; |
||||
|
||||
// Adjust for week rollover
|
||||
if (receiverGpsTowAtTimeOfTransmission < 0) { |
||||
receiverGpsTowAtTimeOfTransmission += SECONDS_IN_WEEK; |
||||
receiverGpsWeek -= 1; |
||||
} else if (receiverGpsTowAtTimeOfTransmission > SECONDS_IN_WEEK) { |
||||
receiverGpsTowAtTimeOfTransmission -= SECONDS_IN_WEEK; |
||||
receiverGpsWeek += 1; |
||||
} |
||||
|
||||
// Compute the satellite clock correction term (Seconds)
|
||||
double clockCorrectionSeconds = |
||||
SatelliteClockCorrectionCalculator.calculateSatClockCorrAndEccAnomAndTkIteratively( |
||||
ephemerisProto, receiverGpsTowAtTimeOfTransmission, |
||||
receiverGpsWeek).satelliteClockCorrectionMeters / SPEED_OF_LIGHT_MPS; |
||||
|
||||
// Correct with the satellite clock correction term
|
||||
double receiverGpsTowAtTimeOfTransmissionCorrectedSec = |
||||
receiverGpsTowAtTimeOfTransmission + clockCorrectionSeconds; |
||||
|
||||
// Adjust for week rollover due to satellite clock correction
|
||||
if (receiverGpsTowAtTimeOfTransmissionCorrectedSec < 0.0) { |
||||
receiverGpsTowAtTimeOfTransmissionCorrectedSec += SECONDS_IN_WEEK; |
||||
receiverGpsWeek -= 1; |
||||
} |
||||
if (receiverGpsTowAtTimeOfTransmissionCorrectedSec > SECONDS_IN_WEEK) { |
||||
receiverGpsTowAtTimeOfTransmissionCorrectedSec -= SECONDS_IN_WEEK; |
||||
receiverGpsWeek += 1; |
||||
} |
||||
return new GpsTimeOfWeekAndWeekNumber(receiverGpsTowAtTimeOfTransmissionCorrectedSec, |
||||
receiverGpsWeek); |
||||
} |
||||
|
||||
/** |
||||
* Calculates the Geometry matrix (describing user to satellite geometry) given a list of |
||||
* satellite positions in ECEF coordinates in meters and the user position in ECEF in meters. |
||||
* |
||||
* <p>The geometry matrix has four columns, and rows equal to the number of satellites. For each |
||||
* of the rows (i.e. for each of the satellites used), the columns are filled with the normalized |
||||
* line–of-sight vectors and 1 s for the fourth column. |
||||
* |
||||
* <p>Source: Parkinson, B.W., Spilker Jr., J.J.: ‘Global positioning system: theory and |
||||
* applications’ page 413 |
||||
*/ |
||||
private static double[][] calculateGeometryMatrix(double[][] satellitePositionsECEFMeters, |
||||
double[] userPositionECEFMeters) { |
||||
|
||||
double[][] geometeryMatrix = new double[satellitePositionsECEFMeters.length][4]; |
||||
for (int i = 0; i < satellitePositionsECEFMeters.length; i++) { |
||||
geometeryMatrix[i][3] = 1; |
||||
} |
||||
// iterate over all satellites
|
||||
for (int i = 0; i < satellitePositionsECEFMeters.length; i++) { |
||||
double[] r = {satellitePositionsECEFMeters[i][0] - userPositionECEFMeters[0], |
||||
satellitePositionsECEFMeters[i][1] - userPositionECEFMeters[1], |
||||
satellitePositionsECEFMeters[i][2] - userPositionECEFMeters[2]}; |
||||
double norm = Math.sqrt(Math.pow(r[0], 2) + Math.pow(r[1], 2) + Math.pow(r[2], 2)); |
||||
for (int j = 0; j < 3; j++) { |
||||
geometeryMatrix[i][j] = |
||||
(userPositionECEFMeters[j] - satellitePositionsECEFMeters[i][j]) / norm; |
||||
} |
||||
} |
||||
return geometeryMatrix; |
||||
} |
||||
|
||||
/** |
||||
* Class containing satellites' PRNs, satellites' positions in ECEF meters, the pseudorange |
||||
* residual per visible satellite in meters and the covariance matrix of the |
||||
* pseudoranges in meters square |
||||
*/ |
||||
protected static class SatellitesPositionPseudorangesResidualAndCovarianceMatrix { |
||||
|
||||
/** Satellites' PRNs */ |
||||
protected final int[] satellitePRNs; |
||||
|
||||
/** ECEF positions (meters) of useful satellites */ |
||||
protected final double[][] satellitesPositionsMeters; |
||||
|
||||
/** Pseudorange measurement residuals (difference of measured to predicted pseudoranges) */ |
||||
protected final double[] pseudorangeResidualsMeters; |
||||
|
||||
/** Pseudorange covariance Matrix for the weighted least squares (meters square) */ |
||||
protected final double[][] covarianceMatrixMetersSquare; |
||||
|
||||
/** Constructor */ |
||||
private SatellitesPositionPseudorangesResidualAndCovarianceMatrix(int[] satellitePRNs, |
||||
double[][] satellitesPositionsMeters, double[] pseudorangeResidualsMeters, |
||||
double[][] covarianceMatrixMetersSquare) { |
||||
this.satellitePRNs = satellitePRNs; |
||||
this.satellitesPositionsMeters = satellitesPositionsMeters; |
||||
this.pseudorangeResidualsMeters = pseudorangeResidualsMeters; |
||||
this.covarianceMatrixMetersSquare = covarianceMatrixMetersSquare; |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Class containing GPS time of week in seconds and GPS week number |
||||
*/ |
||||
private static class GpsTimeOfWeekAndWeekNumber { |
||||
/** GPS time of week in seconds */ |
||||
private final double gpsTimeOfWeekSeconds; |
||||
|
||||
/** GPS week number */ |
||||
private final int weekNumber; |
||||
|
||||
/** Constructor */ |
||||
private GpsTimeOfWeekAndWeekNumber(double gpsTimeOfWeekSeconds, int weekNumber) { |
||||
this.gpsTimeOfWeekSeconds = gpsTimeOfWeekSeconds; |
||||
this.weekNumber = weekNumber; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Uses the common reception time approach to calculate pseudoranges from the time of week |
||||
* measurements reported by the receiver according to http://cdn.intechopen.com/pdfs-wm/27712.pdf.
|
||||
* As well computes the pseudoranges uncertainties for each input satellite |
||||
*/ |
||||
@VisibleForTesting |
||||
static List<GpsMeasurementWithRangeAndUncertainty> computePseudorangeAndUncertainties( |
||||
List<GpsMeasurement> usefulSatellitesToReceiverMeasurements, |
||||
Long[] usefulSatellitesToTOWNs, |
||||
long largestTowNs) { |
||||
|
||||
List<GpsMeasurementWithRangeAndUncertainty> usefulSatellitesToPseudorangeMeasurements = |
||||
Arrays.asList( |
||||
new GpsMeasurementWithRangeAndUncertainty |
||||
[GpsNavigationMessageStore.MAX_NUMBER_OF_SATELLITES]); |
||||
for (int i = 0; i < GpsNavigationMessageStore.MAX_NUMBER_OF_SATELLITES; i++) { |
||||
if (usefulSatellitesToTOWNs[i] != null) { |
||||
double deltai = largestTowNs - usefulSatellitesToTOWNs[i]; |
||||
double pseudorangeMeters = |
||||
(AVERAGE_TRAVEL_TIME_SECONDS + deltai * SECONDS_PER_NANO) * SPEED_OF_LIGHT_MPS; |
||||
|
||||
double signalToNoiseRatioLinear = |
||||
Math.pow(10, usefulSatellitesToReceiverMeasurements.get(i).signalToNoiseRatioDb / 10.0); |
||||
// From Global Positoning System book, Misra and Enge, page 416, the uncertainty of the
|
||||
// pseudorange measurement is calculated next.
|
||||
// For GPS C/A code chip width Tc = 1 microseconds. Narrow correlator with spacing d = 0.1
|
||||
// chip and an average time of DLL correlator T of 20 milliseconds are used.
|
||||
double sigmaMeters = |
||||
SPEED_OF_LIGHT_MPS |
||||
* GPS_CHIP_WIDTH_T_C_SEC |
||||
* Math.sqrt( |
||||
GPS_CORRELATOR_SPACING_IN_CHIPS |
||||
/ (4 * GPS_DLL_AVERAGING_TIME_SEC * signalToNoiseRatioLinear)); |
||||
usefulSatellitesToPseudorangeMeasurements.set( |
||||
i, |
||||
new GpsMeasurementWithRangeAndUncertainty( |
||||
usefulSatellitesToReceiverMeasurements.get(i), pseudorangeMeters, sigmaMeters)); |
||||
} |
||||
} |
||||
return usefulSatellitesToPseudorangeMeasurements; |
||||
} |
||||
|
||||
} |
@ -1 +1 @@
|
||||
include ':app' |
||||
include ':app', ':pseudorange' |
||||
|