com.google.android.apps.location.gps.gnsslogger.PlotFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.google.android.apps.location.gps.gnsslogger.PlotFragment.java

Source

/*
 * 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.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
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("##.#", new DecimalFormatSymbols(Locale.US));

    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);
        }
    }
}