/*
* Copyright (C) 2008-2013 The Android Open Source Project,
* Sean J. Barbeau
*
* 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.android.gpstest;
import com.android.gpstest.util.GnssType;
import com.android.gpstest.util.GpsTestUtil;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.location.GnssMeasurementsEvent;
import android.location.GnssStatus;
import android.location.GpsSatellite;
import android.location.GpsStatus;
import android.location.Location;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.RequiresApi;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import java.util.Iterator;
public class GpsSkyFragment extends Fragment implements GpsTestListener {
private final static String TAG = "GpsSkyFragment";
// View dimensions, to draw the compass with the correct width and height
public static int mHeight;
public static int mWidth;
private GpsSkyView mSkyView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mSkyView = new GpsSkyView(getActivity());
GpsTestActivity.getInstance().addListener(this);
// Get the proper height and width of this view, to ensure the compass draws onscreen
mSkyView.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@SuppressWarnings("deprecation")
@SuppressLint("NewApi")
@Override
public void onGlobalLayout() {
final View v = getView();
mHeight = v.getHeight();
mWidth = v.getWidth();
if (v.getViewTreeObserver().isAlive()) {
// remove this layout listener
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
v.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
v.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
}
}
}
);
return mSkyView;
}
public void onLocationChanged(Location loc) {
}
public void onStatusChanged(String provider, int status, Bundle extras) {
}
public void onProviderEnabled(String provider) {
}
public void onProviderDisabled(String provider) {
}
public void gpsStart() {
}
public void gpsStop() {
}
@Override
public void onGnssFirstFix(int ttffMillis) {
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void onSatelliteStatusChanged(GnssStatus status) {
mSkyView.setGnssStatus(status);
}
@Override
public void onGnssStarted() {
mSkyView.setStarted();
}
@Override
public void onGnssStopped() {
mSkyView.setStopped();
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void onGnssMeasurementsReceived(GnssMeasurementsEvent event) {
mSkyView.setGnssMeasurementEvent(event);
}
@Deprecated
public void onGpsStatusChanged(int event, GpsStatus status) {
switch (event) {
case GpsStatus.GPS_EVENT_STARTED:
mSkyView.setStarted();
break;
case GpsStatus.GPS_EVENT_STOPPED:
mSkyView.setStopped();
break;
case GpsStatus.GPS_EVENT_SATELLITE_STATUS:
mSkyView.setSats(status);
break;
}
}
@Override
public void onOrientationChanged(double orientation, double tilt) {
// For performance reasons, only proceed if this fragment is visible
if (!getUserVisibleHint()) {
return;
}
if (mSkyView != null) {
mSkyView.onOrientationChanged(orientation, tilt);
}
}
@Override
public void onNmeaMessage(String message, long timestamp) {
}
private static class GpsSkyView extends View implements GpsTestListener {
private static final float PRN_TEXT_SCALE = 0.7f;
private static int SAT_RADIUS;
private final float mSnrThresholds[];
private final int mSnrColors[];
private final float mCn0Thresholds[];
private final int mCn0Colors[];
private boolean mUseSnr = false;
Context mContext;
WindowManager mWindowManager;
private Paint mHorizonActiveFillPaint, mHorizonInactiveFillPaint, mHorizonStrokePaint,
mGridStrokePaint,
mSatelliteFillPaint, mSatelliteStrokePaint, mSatelliteUsedStrokePaint,
mNorthPaint, mNorthFillPaint, mPrnIdPaint, mNotInViewPaint;
private double mOrientation = 0.0;
private boolean mStarted;
private float mSnrCn0s[], mElevs[], mAzims[]; // Holds either SNR or C/N0 - see #65
private boolean mHasEphemeris[], mHasAlmanac[], mUsedInFix[];
private int mPrns[], mConstellationType[];
private int mSvCount;
public GpsSkyView(Context context) {
super(context);
mContext = context;
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
SAT_RADIUS = GpsTestUtil.dpToPixels(context, 5);
mHorizonActiveFillPaint = new Paint();
mHorizonActiveFillPaint.setColor(Color.WHITE);
mHorizonActiveFillPaint.setStyle(Paint.Style.FILL);
mHorizonActiveFillPaint.setAntiAlias(true);
mHorizonInactiveFillPaint = new Paint();
mHorizonInactiveFillPaint.setColor(Color.LTGRAY);
mHorizonInactiveFillPaint.setStyle(Paint.Style.FILL);
mHorizonInactiveFillPaint.setAntiAlias(true);
mHorizonStrokePaint = new Paint();
mHorizonStrokePaint.setColor(Color.BLACK);
mHorizonStrokePaint.setStyle(Paint.Style.STROKE);
mHorizonStrokePaint.setStrokeWidth(2.0f);
mHorizonStrokePaint.setAntiAlias(true);
mGridStrokePaint = new Paint();
mGridStrokePaint.setColor(Color.GRAY);
mGridStrokePaint.setStyle(Paint.Style.STROKE);
mGridStrokePaint.setAntiAlias(true);
mSatelliteFillPaint = new Paint();
mSatelliteFillPaint.setColor(Color.YELLOW);
mSatelliteFillPaint.setStyle(Paint.Style.FILL);
mSatelliteFillPaint.setAntiAlias(true);
mSatelliteStrokePaint = new Paint();
mSatelliteStrokePaint.setColor(Color.BLACK);
mSatelliteStrokePaint.setStyle(Paint.Style.STROKE);
mSatelliteStrokePaint.setStrokeWidth(2.0f);
mSatelliteStrokePaint.setAntiAlias(true);
mSatelliteUsedStrokePaint = new Paint();
mSatelliteUsedStrokePaint.setColor(Color.BLACK);
mSatelliteUsedStrokePaint.setStyle(Paint.Style.STROKE);
mSatelliteUsedStrokePaint.setStrokeWidth(8.0f);
mSatelliteUsedStrokePaint.setAntiAlias(true);
mSnrThresholds = new float[]{0.0f, 10.0f, 20.0f, 30.0f};
mSnrColors = new int[]{Color.GRAY, Color.RED, Color.YELLOW, Color.GREEN};
mCn0Thresholds = new float[]{0.0f, 16.6f, 33.3f, 50.0f};
mCn0Colors = new int[]{Color.GRAY, Color.RED, Color.YELLOW, Color.GREEN};
mNorthPaint = new Paint();
mNorthPaint.setColor(Color.BLACK);
mNorthPaint.setStyle(Paint.Style.STROKE);
mNorthPaint.setStrokeWidth(4.0f);
mNorthPaint.setAntiAlias(true);
mNorthFillPaint = new Paint();
mNorthFillPaint.setColor(Color.GRAY);
mNorthFillPaint.setStyle(Paint.Style.FILL);
mNorthFillPaint.setStrokeWidth(4.0f);
mNorthFillPaint.setAntiAlias(true);
mPrnIdPaint = new Paint();
mPrnIdPaint.setColor(Color.BLACK);
mPrnIdPaint.setStyle(Paint.Style.STROKE);
mPrnIdPaint
.setTextSize(GpsTestUtil.dpToPixels(getContext(), SAT_RADIUS * PRN_TEXT_SCALE));
mPrnIdPaint.setAntiAlias(true);
mNotInViewPaint = new Paint();
mNotInViewPaint.setColor(ContextCompat.getColor(context, R.color.not_in_view_sat));
mNotInViewPaint.setStyle(Paint.Style.FILL);
mNotInViewPaint.setStrokeWidth(4.0f);
mNotInViewPaint.setAntiAlias(true);
setFocusable(true);
}
public void setStarted() {
mStarted = true;
invalidate();
}
public void setStopped() {
mStarted = false;
mSvCount = 0;
invalidate();
}
@RequiresApi(api = Build.VERSION_CODES.N)
public void setGnssStatus(GnssStatus status) {
// Use C/N0 instead of SNR - see #65
mUseSnr = false;
if (mPrns == null) {
/**
* We need to allocate arrays big enough so we don't overflow them. Per
* https://developer.android.com/reference/android/location/GnssStatus.html#getSvid(int)
* 255 should be enough to contain all known satellites world-wide.
*/
final int MAX_LENGTH = 255;
mPrns = new int[MAX_LENGTH];
mSnrCn0s = new float[MAX_LENGTH];
mElevs = new float[MAX_LENGTH];
mAzims = new float[MAX_LENGTH];
mConstellationType = new int[MAX_LENGTH];
mHasEphemeris = new boolean[MAX_LENGTH];
mHasAlmanac = new boolean[MAX_LENGTH];
mUsedInFix = new boolean[MAX_LENGTH];
}
int length = status.getSatelliteCount();
mSvCount = 0;
while (mSvCount < length) {
mSnrCn0s[mSvCount] = status.getCn0DbHz(mSvCount);
mElevs[mSvCount] = status.getElevationDegrees(mSvCount);
mAzims[mSvCount] = status.getAzimuthDegrees(mSvCount);
mPrns[mSvCount] = status.getSvid(mSvCount);
mConstellationType[mSvCount] = status.getConstellationType(mSvCount);
mHasEphemeris[mSvCount] = status.hasEphemerisData(mSvCount);
mHasAlmanac[mSvCount] = status.hasAlmanacData(mSvCount);
mUsedInFix[mSvCount] = status.usedInFix(mSvCount);
mSvCount++;
}
mStarted = true;
invalidate();
}
@RequiresApi(api = Build.VERSION_CODES.N)
public void setGnssMeasurementEvent(GnssMeasurementsEvent event) {
// No-op
}
@Deprecated
public void setSats(GpsStatus status) {
// Use SNR instead of C/N0 - see #65
mUseSnr = true;
Iterator<GpsSatellite> satellites = status.getSatellites().iterator();
if (mSnrCn0s == null) {
int length = status.getMaxSatellites();
mSnrCn0s = new float[length];
mElevs = new float[length];
mAzims = new float[length];
mPrns = new int[length];
mHasEphemeris = new boolean[length];
mHasAlmanac = new boolean[length];
mUsedInFix = new boolean[length];
// Constellation type isn't used, but instantiate it to avoid NPE in legacy devices
mConstellationType = new int[length];
}
mSvCount = 0;
while (satellites.hasNext()) {
GpsSatellite satellite = satellites.next();
mSnrCn0s[mSvCount] = satellite.getSnr();
mElevs[mSvCount] = satellite.getElevation();
mAzims[mSvCount] = satellite.getAzimuth();
mPrns[mSvCount] = satellite.getPrn();
mHasEphemeris[mSvCount] = satellite.hasEphemeris();
mHasAlmanac[mSvCount] = satellite.hasAlmanac();
mUsedInFix[mSvCount] = satellite.usedInFix();
mSvCount++;
}
mStarted = true;
invalidate();
}
private void drawLine(Canvas c, float x1, float y1, float x2, float y2) {
// rotate the line based on orientation
double angle = Math.toRadians(-mOrientation);
float cos = (float) Math.cos(angle);
float sin = (float) Math.sin(angle);
float centerX = (x1 + x2) / 2.0f;
float centerY = (y1 + y2) / 2.0f;
x1 -= centerX;
y1 = centerY - y1;
x2 -= centerX;
y2 = centerY - y2;
float X1 = cos * x1 + sin * y1 + centerX;
float Y1 = -(-sin * x1 + cos * y1) + centerY;
float X2 = cos * x2 + sin * y2 + centerX;
float Y2 = -(-sin * x2 + cos * y2) + centerY;
c.drawLine(X1, Y1, X2, Y2, mGridStrokePaint);
}
private void drawHorizon(Canvas c, int s) {
float radius = s / 2;
c.drawCircle(radius, radius, radius,
mStarted ? mHorizonActiveFillPaint : mHorizonInactiveFillPaint);
drawLine(c, 0, radius, 2 * radius, radius);
drawLine(c, radius, 0, radius, 2 * radius);
c.drawCircle(radius, radius, elevationToRadius(s, 60.0f), mGridStrokePaint);
c.drawCircle(radius, radius, elevationToRadius(s, 30.0f), mGridStrokePaint);
c.drawCircle(radius, radius, elevationToRadius(s, 0.0f), mGridStrokePaint);
c.drawCircle(radius, radius, radius, mHorizonStrokePaint);
}
private void drawNorthIndicator(Canvas c, int s) {
float radius = s / 2;
double angle = Math.toRadians(-mOrientation);
final float ARROW_HEIGHT_SCALE = 0.05f;
final float ARROW_WIDTH_SCALE = 0.1f;
float x1, y1; // Tip of arrow
x1 = radius;
y1 = elevationToRadius(s, 90.0f);
float x2, y2;
x2 = x1 + radius * ARROW_HEIGHT_SCALE;
y2 = y1 + radius * ARROW_WIDTH_SCALE;
float x3, y3;
x3 = x1 - radius * ARROW_HEIGHT_SCALE;
y3 = y1 + radius * ARROW_WIDTH_SCALE;
Path path = new Path();
path.setFillType(Path.FillType.EVEN_ODD);
path.moveTo(x1, y1);
path.lineTo(x2, y2);
path.lineTo(x3, y3);
path.lineTo(x1, y1);
path.close();
// Rotate arrow around center point
Matrix matrix = new Matrix();
matrix.postRotate((float) -mOrientation, radius, radius);
path.transform(matrix);
c.drawPath(path, mNorthPaint);
c.drawPath(path, mNorthFillPaint);
}
private void drawSatellite(Canvas c, int s, float elev, float azim, float snrCn0, int prn,
int constellationType, boolean usedInFix) {
double radius, angle;
float x, y;
// Place PRN text slightly below drawn satellite
final double PRN_X_SCALE = 1.4;
final double PRN_Y_SCALE = 3.8;
Paint fillPaint;
if (snrCn0 == 0.0f) {
// Satellite can't be seen
fillPaint = mNotInViewPaint;
} else {
// Calculate fill color based on signal strength
fillPaint = getSatellitePaint(mSatelliteFillPaint, snrCn0);
}
Paint strokePaint;
if (usedInFix) {
strokePaint = mSatelliteUsedStrokePaint;
} else {
strokePaint = mSatelliteStrokePaint;
}
radius = elevationToRadius(s, elev);
azim -= mOrientation;
angle = (float) Math.toRadians(azim);
x = (float) ((s / 2) + (radius * Math.sin(angle)));
y = (float) ((s / 2) - (radius * Math.cos(angle)));
// Change shape based on satellite operator
GnssType operator;
if (GpsTestUtil.isGnssStatusListenerSupported()) {
operator = GpsTestUtil.getGnssConstellationType(constellationType);
} else {
operator = GpsTestUtil.getGnssType(prn);
}
switch (operator) {
case NAVSTAR:
c.drawCircle(x, y, SAT_RADIUS, fillPaint);
c.drawCircle(x, y, SAT_RADIUS, strokePaint);
break;
case GLONASS:
c.drawRect(x - SAT_RADIUS, y - SAT_RADIUS, x + SAT_RADIUS, y + SAT_RADIUS,
fillPaint);
c.drawRect(x - SAT_RADIUS, y - SAT_RADIUS, x + SAT_RADIUS, y + SAT_RADIUS,
strokePaint);
break;
case QZSS:
drawTriangle(c, x, y, fillPaint, strokePaint);
break;
case BEIDOU:
drawPentagon(c, x, y, fillPaint, strokePaint);
break;
case GALILEO:
// We're running out of shapes - QZSS should be regional to Japan, so re-use triangle
drawTriangle(c, x, y, fillPaint, strokePaint);
break;
}
c.drawText(String.valueOf(prn), x - (int) (SAT_RADIUS * PRN_X_SCALE),
y + (int) (SAT_RADIUS * PRN_Y_SCALE), mPrnIdPaint);
}
private float elevationToRadius(int s, float elev) {
return ((s / 2) - SAT_RADIUS) * (1.0f - (elev / 90.0f));
}
private void drawTriangle(Canvas c, float x, float y, Paint fillPaint, Paint strokePaint) {
float x1, y1; // Top
x1 = x;
y1 = y - SAT_RADIUS;
float x2, y2; // Lower left
x2 = x - SAT_RADIUS;
y2 = y + SAT_RADIUS;
float x3, y3; // Lower right
x3 = x + SAT_RADIUS;
y3 = y + SAT_RADIUS;
Path path = new Path();
path.setFillType(Path.FillType.EVEN_ODD);
path.moveTo(x1, y1);
path.lineTo(x2, y2);
path.lineTo(x3, y3);
path.lineTo(x1, y1);
path.close();
c.drawPath(path, fillPaint);
c.drawPath(path, strokePaint);
}
private void drawPentagon(Canvas c, float x, float y, Paint fillPaint, Paint strokePaint) {
Path path = new Path();
path.moveTo(x, y - SAT_RADIUS);
path.lineTo(x - SAT_RADIUS, y - (SAT_RADIUS / 3));
path.lineTo(x - 2 * (SAT_RADIUS / 3), y + SAT_RADIUS);
path.lineTo(x + 2 * (SAT_RADIUS / 3), y + SAT_RADIUS);
path.lineTo(x + SAT_RADIUS, y - (SAT_RADIUS / 3));
path.close();
c.drawPath(path, fillPaint);
c.drawPath(path, strokePaint);
}
/**
* Gets the paint color for a satellite based on provided SNR or C/N0
*
* @param base the base paint color to be changed
* @param snrCn0 the SNR to use (if mUseSnr is true) or the C/N0 to use (if mUseSnr is
* false)
* to generate the satellite color based on signal quality
* @return the paint color for a satellite based on provided SNR or C/N0
*/
private Paint getSatellitePaint(Paint base, float snrCn0) {
Paint newPaint;
newPaint = new Paint(base);
int numSteps;
final float thresholds[];
final int colors[];
if (mUseSnr) {
// Use SNR
numSteps = mSnrThresholds.length;
thresholds = mSnrThresholds;
colors = mSnrColors;
} else {
// Use C/N0
numSteps = mCn0Thresholds.length;
thresholds = mCn0Thresholds;
colors = mCn0Colors;
}
if (snrCn0 <= thresholds[0]) {
newPaint.setColor(colors[0]);
return newPaint;
}
if (snrCn0 >= thresholds[numSteps - 1]) {
newPaint.setColor(colors[numSteps - 1]);
return newPaint;
}
for (int i = 0; i < numSteps - 1; i++) {
float threshold = thresholds[i];
float nextThreshold = thresholds[i + 1];
if (snrCn0 >= threshold && snrCn0 <= nextThreshold) {
int c1, r1, g1, b1, c2, r2, g2, b2, c3, r3, g3, b3;
float f;
c1 = colors[i];
r1 = Color.red(c1);
g1 = Color.green(c1);
b1 = Color.blue(c1);
c2 = colors[i + 1];
r2 = Color.red(c2);
g2 = Color.green(c2);
b2 = Color.blue(c2);
f = (snrCn0 - threshold) / (nextThreshold - threshold);
r3 = (int) (r2 * f + r1 * (1.0f - f));
g3 = (int) (g2 * f + g1 * (1.0f - f));
b3 = (int) (b2 * f + b1 * (1.0f - f));
c3 = Color.rgb(r3, g3, b3);
newPaint.setColor(c3);
return newPaint;
}
}
newPaint.setColor(Color.MAGENTA);
return newPaint;
}
@Override
protected void onDraw(Canvas canvas) {
int minScreenDimen;
minScreenDimen = (GpsSkyFragment.mWidth < GpsSkyFragment.mHeight)
? GpsSkyFragment.mWidth : GpsSkyFragment.mHeight;
drawHorizon(canvas, minScreenDimen);
drawNorthIndicator(canvas, minScreenDimen);
if (mElevs != null) {
int numSats = mSvCount;
for (int i = 0; i < numSats; i++) {
if (mElevs[i] != 0.0f || mAzims[i] != 0.0f) {
drawSatellite(canvas, minScreenDimen, mElevs[i], mAzims[i], mSnrCn0s[i],
mPrns[i], mConstellationType[i], mUsedInFix[i]);
}
}
}
}
@Override
public void onOrientationChanged(double orientation, double tilt) {
mOrientation = orientation;
invalidate();
}
@Override
public void gpsStart() {
}
@Override
public void gpsStop() {
}
@Override
public void onGnssFirstFix(int ttffMillis) {
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void onSatelliteStatusChanged(GnssStatus status) {
}
@Override
public void onGnssStarted() {
}
@Override
public void onGnssStopped() {
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void onGnssMeasurementsReceived(GnssMeasurementsEvent event) {
}
@Override
public void onNmeaMessage(String message, long timestamp) {
}
@Deprecated
@Override
public void onGpsStatusChanged(int event, GpsStatus status) {
}
@Override
public void onLocationChanged(Location location) {
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
@Override
public void onProviderEnabled(String provider) {
}
@Override
public void onProviderDisabled(String provider) {
}
}
}