首先介绍一下相关的概念:
方位角:是从某点的指北(地理北极)方向线起,依顺时针方向到目标方向线之间的水平夹角。
高度角:从一点至观测目标的方向线与水平面间的夹角。
卫星天顶图:即是根据每一颗卫星的方位角和高度角将其画在以观测位置为中心的天顶图上。
天顶图其底图为由外向内的三个圆和四条直线。三个圆由外向内依次代表高度角0°、30°、60°,中心点代表90°;四条直线分别表示正北-正南、东北-西南、正东-正西、东南-西北的方位角方向。
下面逐一介绍绘制的步骤
要想画出卫星图,必须先有卫星的方位角和高度角等基本信息。在Android中,可以通过向LocationManager类注册GnssStatus监听器获取卫星PRN、方位角、高度角、星座类型、卫星载噪比等与卫星本身有关的信息。
关于如何注册GNSS监听器,Android GNSS伪距计算 中有注册测量监听器的详细步骤,卫星状态监听器的注册方式完全类似。
不同的是在回调方法中的行为,在这里,我们需要使用数组将卫星信息保存起来,如下:
private float[] mElevations, mAzimuths, mSnrs;
private int[] mPrns, mConstellationTypes;
public void setGnssStatus(GnssStatus status) {
if (mPrns == null) {
final int MAX_LENGTH = 255;
mPrns = new int[MAX_LENGTH];
mElevations = new float[MAX_LENGTH];
mAzimuths = new float[MAX_LENGTH];
mConstellationTypes = new int[MAX_LENGTH];
mSnrs = new float[MAX_LENGTH];
}
int length = status.getSatelliteCount();
mSvCount = 0;
while (mSvCount < length) {
mPrns[mSvCount] = status.getSvid(mSvCount);
mElevations[mSvCount] = status.getElevationDegrees(mSvCount);
mAzimuths[mSvCount] = status.getAzimuthDegrees(mSvCount);
mConstellationTypes[mSvCount] = status.getConstellationType(mSvCount);
mSnrs[mSvCount] = status.getCn0DbHz(mSvCount);
mSvCount++;
}
invalidate();
}
通过GnssStatus的参数对象status的 getSatelliteCount() 方法获取卫星数量,然后使用 getSvid()、getElevationDegrees()、getAzimuthDegrees()、getConstellationType()、getCn0DbHz() 方法分别获取卫星的 PRN、高度角、方位角、星座、载噪比。
然后调用View对象的invalidate()方法重绘星空图,即更新。
由于Android中并没有一种系统控件可以让我们方便的展示卫星图,因此我们需要自定义一个天空图视图
public class GnssSkyView extends View { }
我们在碎片Fragment中加载这个视图,通过在碎片上注册监听器获得不断更新的信息,并在碎片的回调方法中调用 GnssSkyView 对象的 setGnssStatus 方法将信息传给它,从而使其获得数据来源,进而进行多样化地展示
底图包括三个圆、四条直线和外圈圆上的方位角刻度。
onDraw() 方法会在视图绘制的过程中系统自动调用,我们需要在这个方法中自定义绘制内容。
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int minScreenDimen = Math.min(mWidth, mHeight);
float radius = minScreenDimen / 2.0f;
drawCircle(canvas, radius);
drawLine(canvas, minScreenDimen, radius);
drawDegree(canvas, radius);
}
因为我们要画的是一个圆,所以一定有一个外切正方形,上面的代码中,minScreenDimen 为视图长和宽的最小值,这个值就作为正方形的边长,也是外圈圆的直径。
然后我们分别调用三个函数drawCircle()、drawLine()、drawDegree(),这三个函数是我们自己定义的,分别画圆、直线、刻度。
onDraw() 方法会给我们一个参数canvas,这个参数是视图画布Canvas的对象,通过调用canvas的 drawCircle() 方法,就能实现圆的绘制。
该方法的函数原型如下:
public void drawCircle(float cx, float cy, float radius, Paint paint) {
super.drawCircle(cx, cy, radius, paint);
}
可见,我们需要给出圆心的 x,y 坐标和圆的半径 r,还有画笔 paint。
这里需要注意的是,画布的坐标系是以左上角为坐标原点,水平向右为X轴正向,竖直向下为Y轴正向。
我们在前面已经获取了外圆的半径,而外圈圆代表的是高度角0°,因此还需要计算出30°、60°的高度角代表的圆的半径。
然后这里给了一个 Y_TRANSLATION ,这是预定义的Y轴偏移量,方向向下为正,目的是使最上方的卫星能够完整的显示出来。
private void drawCircle(Canvas c, float radius) {
c.drawCircle(radius, radius + Y_TRANSLATION, elevationToRadius(radius, 60.0f), mPaintCircleAndLine);
c.drawCircle(radius, radius + Y_TRANSLATION, elevationToRadius(radius, 30.0f), mPaintCircleAndLine);
c.drawCircle(radius, radius + Y_TRANSLATION, elevationToRadius(radius, 0.0f), mPaintCircleAndLine);
}
private float elevationToRadius(float s, float elev) {
return s * (1.0f - (elev / 90.0f));
}
直线的画法与圆类似,画直线的系统函数如下,其属于canvas画布对象:
public void drawLine(float startX, float startY, float stopX, float stopY, Paint paint) {
super.drawLine(startX, startY, stopX, stopY, paint);
}
可知我们需要给出线段的起点坐标和终点坐标,还有画笔对象 paint
先计算出四条直线的起止点坐标,然后调用drawLine()方法
下面的函数中,s即minScreenDimen
private void drawLine(Canvas c, int s, float radius) {
c.drawLine(radius, Y_TRANSLATION, radius, s + Y_TRANSLATION, mPaintCircleAndLine);
c.drawLine(0, radius + Y_TRANSLATION, s, radius + Y_TRANSLATION, mPaintCircleAndLine);
final float cos45 = (float) Math.cos(Math.PI / 4);
float d1 = radius * (1 - cos45);
float d2 = radius * (1 + cos45);
c.drawLine(d1, d1 + Y_TRANSLATION, d2, d2 + Y_TRANSLATION, mPaintCircleAndLine);
c.drawLine(d2, d1 + Y_TRANSLATION, d1, d2 + Y_TRANSLATION, mPaintCircleAndLine);
}
绘制刻度的实质就是绘制直线,计算出每一个刻度线段的起止点坐标,然后调用绘制直线的函数
这里,在正北、正东、正南、正西方向上还要给出N、E、S、W的文本,可以调用画布对象canvas的drawText()系统函数绘制
drawText()系统函数原型如下:
public void drawText(String text, float x, float y, Paint paint) {
super.drawText(text, x, y, paint);
}
只需给出文本字符串、文本位置坐标、画笔对象
这里,使用rorate()系统函数进行旋转绘制,详细原理可以自行百度
private void drawDegree(Canvas c, float radius) {
for (int i = 0; i < 360; i += 15) {
if (i == 45 || i == 135 || i == 225 || i == 315) {
c.drawText(String.valueOf(i), radius, 40 + Y_TRANSLATION, mPaintDegree);
} else if (i == 0) {
c.drawText("N", radius, 40 + Y_TRANSLATION, mPaintDegree);
} else if (i == 90) {
c.drawText("E", radius, 40 + Y_TRANSLATION, mPaintDegree);
} else if (i == 180) {
c.drawText("S", radius, 40 + Y_TRANSLATION, mPaintDegree);
} else if (i == 270) {
c.drawText("W", radius, 40 + Y_TRANSLATION, mPaintDegree);
} else {
c.drawLine(radius, Y_TRANSLATION, radius, 20 + Y_TRANSLATION, mPaintDegree);
}
c.rotate(15, radius, radius + Y_TRANSLATION);
}
}
如上图,根据高度角可以计算出卫星的半径,即到圆心的距离 r,再根据方位角 α 和外圈圆的半径 R 即可确定卫星在画布坐标系中的坐标。
这个比较简单,只需要预先准备好四大系统和日本的代表旗帜,然后根据星座类型来获取Bitmap对象
这里,准备了六张PNG图片,放在./res/drawable目录下
然后,使用BitmapFactory.decodeResource() 方法创建一个位图对象。这个方法的第一个参数为Android系统的资源对象resources,可以通过视图View对象的getResources() 方法得到;第二个参数为图片数据的资源ID
值得一提的是,一般我们准备的图片尺寸都各不相同,并且远大于在视图上展示的大小,而为了让所有星座的卫星使用同样小尺寸的图片,我们需要对位图Bitmap进行放缩。
private Bitmap getSatelliteBitmap(int constellationType) {
Bitmap baseMap, newMap;
int width, height;
switch (constellationType) {
case GnssStatus.CONSTELLATION_BEIDOU:
baseMap = BitmapFactory.decodeResource(getResources(), R.drawable.flag_of_china);
break;
case GnssStatus.CONSTELLATION_GPS:
baseMap = BitmapFactory.decodeResource(getResources(), R.drawable.flag_of_america);
break;
case GnssStatus.CONSTELLATION_GALILEO:
baseMap = BitmapFactory.decodeResource(getResources(), R.drawable.flag_of_europe);
break;
case GnssStatus.CONSTELLATION_GLONASS:
baseMap = BitmapFactory.decodeResource(getResources(), R.drawable.flag_of_russia);
break;
case GnssStatus.CONSTELLATION_QZSS:
baseMap = BitmapFactory.decodeResource(getResources(), R.drawable.flag_of_japan);
break;
default:
baseMap = BitmapFactory.decodeResource(getResources(), R.drawable.flag_of_other);
}
width = baseMap.getWidth();
height = baseMap.getHeight();
newMap = UiUtils.scaling(baseMap, (SAT_RADIUS * 2.0f) / width, (SAT_RADIUS * 2.0f) / height);
return newMap;
}
public class UiUtils {
public static Bitmap scaling(Bitmap bitmap, float widthScale, float heightScale) {
Matrix matrix = new Matrix();
matrix.postScale(widthScale, heightScale); //长和宽放大缩小的比例
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
}
绘制卫星的代码如下,其中我们还要生成卫星星座+卫星PRN的文本字符串:
private void drawSatellite(Canvas c, int s, float elev, float azim, float snr, int prn, int constellationType) {
double radius, angle;
float x, y;
Bitmap satMap;
satMap = getSatelliteBitmap(constellationType);
String satText;
satText = getSatelliteText(prn, constellationType);
radius = elevationToRadius(s / 2.0f, elev);
angle = (float) Math.toRadians(azim);
x = (float) ((s / 2.0f) + (radius * Math.sin(angle)));
y = (float) ((s / 2.0f) - (radius * Math.cos(angle)));
c.drawBitmap(satMap, x - SAT_RADIUS, y - SAT_RADIUS + Y_TRANSLATION, null);
c.drawText(satText, x - SAT_RADIUS, y + SAT_RADIUS * 2 + Y_TRANSLATION, mPrnIdPaint);
}
private String getSatelliteText(int prn, int constellationType) {
StringBuilder builder = new StringBuilder();
switch (constellationType) {
case GnssStatus.CONSTELLATION_BEIDOU:
builder.append("C");
break;
case GnssStatus.CONSTELLATION_GPS:
builder.append("G");
break;
case GnssStatus.CONSTELLATION_GALILEO:
builder.append("E");
break;
case GnssStatus.CONSTELLATION_GLONASS:
builder.append("R");
break;
case GnssStatus.CONSTELLATION_QZSS:
builder.append("Q");
break;
default:
builder.append("S");
}
builder.append(prn);
return builder.toString();
}
我们在注册卫星状态信息的监听器后,会根据注册时指定的更新参数获得一定时间间隔的卫星信息,每次卫星的状态信息得到更新后,我们调用天空图类对象(自定义视图)的 invalidate() 方法(View及其子类的对象都有这个方法)重绘视图即可更新图上卫星的位置。
View的工作流程,主要是measure、layout和draw三步,measure用来测量View的宽高,layout用来确定View(在ViewGroup中)的位置,draw则用来绘制View。
决定View的大小只需要两个值:宽详细测量值(widthMeasureSpec)和高详细测量值(heightMeasureSpec)。也可以把详细测量值理解为视图View想要的大小说明(想要的未必就是最终大小)。
我们先获取屏幕的宽高,然后取较小值,因为一般是宽较小,所以将高度设为宽度+20dp,然后使用View的 setMeasuredDimension() 方法设置视图的大小。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int windowWidth = mDisplayMetrics.widthPixels;
int windowHeight = mDisplayMetrics.heightPixels;
int minL = Math.min(windowWidth, windowHeight);
setMeasuredDimension(minL, minL + 20);
}
private void init(Context context) {
mDisplayMetrics = mContext.getResources().getDisplayMetrics();
getViewTreeObserver().addOnPreDrawListener(
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
mHeight = getHeight();
mWidth = getWidth();
return true;
}
}
);
invalidate();
}