content: /*
   * Copyright (C) 2007 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.androzic.location;
  
  import java.util.Calendar;
  import java.util.GregorianCalendar;
  import java.util.TimeZone;
  
  import android.location.Location;
  import android.os.Bundle;
  import android.util.Log;
  
  /**
   * {@hide}
   */
  public class NmeaParser {
  
      private static final String TAG = "NmeaParser";
  
      private static final TimeZone sUtcTimeZone = TimeZone.getTimeZone("UTC");
  
      private static final float KNOTS_TO_METERS_PER_SECOND = 0.51444444444f;
  
      private final String mName;
  
      private int mYear = -1;
      private int mMonth;
      private int mDay;
  
      private long mTime = -1;
      private long mBaseTime;
      private double mLatitude;
      private double mLongitude;
  
      private boolean mHasAltitude;
      private double mAltitude;
      private boolean mHasBearing;
      private float mBearing;
      private boolean mHasSpeed;
      private float mSpeed;
  
      private boolean mNewWaypoint = false;
      private Location mLocation = null;
      private Bundle mExtras;
  
      public NmeaParser(String name) {
          mName = name;
      }
  
      private boolean updateTime(String time) {
          if (time.length() < 6) {
              return false;
          }
          if (mYear == -1) {
              // Since we haven't seen a day/month/year yet,
              // we can't construct a meaningful time stamp.
              // Clean up any old data.
              mLatitude = 0.0;
              mLongitude = 0.0;
              mHasAltitude = false;
              mHasBearing = false;
              mHasSpeed = false;
              mExtras = null;
              return false;
          }
  
          int hour, minute;
          float second;
          try {
              hour = Integer.parseInt(time.substring(0, 2));
              minute = Integer.parseInt(time.substring(2, 4));
              second = Float.parseFloat(time.substring(4, time.length()));
          } catch (NumberFormatException nfe) {
              Log.e(TAG, "Error parsing timestamp " + time);
              return false;
          }
  
          int isecond = (int) second;
          int millis = (int) ((second - isecond) * 1000);
          Calendar c = new GregorianCalendar(sUtcTimeZone);
          c.set(mYear, mMonth, mDay, hour, minute, isecond);
          long newTime = c.getTimeInMillis() + millis;
  
          if (mTime == -1) {
              mTime = 0;
              mBaseTime = newTime;
          }
          newTime -= mBaseTime;
  
          // If the timestamp has advanced, copy the temporary data
          // into a new Location
          if (newTime != mTime) {
              mNewWaypoint = true;
              mLocation = new Location(mName);
              mLocation.setTime(mTime);
              mLocation.setLatitude(mLatitude);
              mLocation.setLongitude(mLongitude);
              if (mHasAltitude) {
                  mLocation.setAltitude(mAltitude);
              }
              if (mHasBearing) {
                  mLocation.setBearing(mBearing);
              }
              if (mHasSpeed) {
                  mLocation.setSpeed(mSpeed);
              }
              mLocation.setExtras(mExtras);
              mExtras = null;
  
              mTime = newTime;
              mHasAltitude = false;
              mHasBearing = false;
              mHasSpeed = false;
          }
          return true;
      }
  
      private boolean updateDate(String date) {
          if (date.length() != 6) {
              return false;
          }
          int month, day, year;
          try {
              day = Integer.parseInt(date.substring(0, 2));
              month = Integer.parseInt(date.substring(2, 4));
              year = 2000 + Integer.parseInt(date.substring(4, 6));
          } catch (NumberFormatException nfe) {
              Log.e(TAG, "Error parsing date " + date);
              return false;
          }
  
          mYear = year;
          mMonth = month;
          mDay = day;
          return true;
      }
  
      private boolean updateTime(String time, String date) {
          if (!updateDate(date)) {
                  return false;
          }
          return updateTime(time);
      }
  
      private boolean updateIntExtra(String name, String value) {
          int val;
          try {
              val = Integer.parseInt(value);
          } catch (NumberFormatException nfe) {
              Log.e(TAG, "Exception parsing int " + name + ": " + value, nfe);
              return false;
          }
          if (mExtras == null) {
              mExtras = new Bundle();
          }
          mExtras.putInt(name, val);
          return true;
      }
  
      private boolean updateFloatExtra(String name, String value) {
          float val;
          try {
              val = Float.parseFloat(value);
          } catch (NumberFormatException nfe) {
              Log.e(TAG, "Exception parsing float " + name + ": " + value, nfe);
              return false;
          }
          if (mExtras == null) {
              mExtras = new Bundle();
          }
          mExtras.putFloat(name, val);
          return true;
      }
  
      private boolean updateDoubleExtra(String name, String value) {
          double val;
          try {
              val = Double.parseDouble(value);
          } catch (NumberFormatException nfe) {
              Log.e(TAG, "Exception parsing double " + name + ": " + value, nfe);
              return false;
          }
          if (mExtras == null) {
              mExtras = new Bundle();
          }
          mExtras.putDouble(name, val);
          return true;
      }
  
      private double convertFromHHMM(String coord) {
          double val = Double.parseDouble(coord);
          int degrees = ((int) Math.floor(val)) / 100;
          double minutes = val - (degrees * 100);
          double dcoord = degrees + minutes / 60.0;
          return dcoord;
      }
  
      private boolean updateLatLon(String latitude, String latitudeHemi,
          String longitude, String longitudeHemi) {
          if (latitude.length() == 0 || longitude.length() == 0) {
              return false;
          }
  
          // Lat/long values are expressed as {D}DDMM.MMMM
          double lat, lon;
          try {
              lat = convertFromHHMM(latitude);
              if (latitudeHemi.charAt(0) == 'S') {
                  lat = -lat;
              }
          } catch (NumberFormatException nfe1) {
              Log.e(TAG, "Exception parsing lat/long: " + nfe1, nfe1);
              return false;
          }
  
          try {
              lon = convertFromHHMM(longitude);
              if (longitudeHemi.charAt(0) == 'W') {
                  lon = -lon;
              }
          } catch (NumberFormatException nfe2) {
              Log.e(TAG, "Exception parsing lat/long: " + nfe2, nfe2);
              return false;
          }
  
          // Only update if both were parsed cleanly
          mLatitude = lat;
          mLongitude = lon;
          return true;
      }
  
      private boolean updateAltitude(String altitude) {
          if (altitude.length() == 0) {
              return false;
          }
          double alt;
          try {
              alt = Double.parseDouble(altitude);
          } catch (NumberFormatException nfe) {
              Log.e(TAG, "Exception parsing altitude " + altitude + ": " + nfe,
                    nfe);
              return false;
          }
  
          mHasAltitude = true;
          mAltitude = alt;
          return true;
      }
  
      private boolean updateBearing(String bearing) {
          float brg;
          try {
              brg = Float.parseFloat(bearing);
          } catch (NumberFormatException nfe) {
              Log.e(TAG, "Exception parsing bearing " + bearing + ": " + nfe,
                    nfe);
              return false;
          }
  
          mHasBearing = true;
          mBearing = brg;
          return true;
      }
  
      private boolean updateSpeed(String speed) {
          float spd;
          try {
              spd = Float.parseFloat(speed) * KNOTS_TO_METERS_PER_SECOND;
          } catch (NumberFormatException nfe) {
              Log.e(TAG, "Exception parsing speed " + speed + ": " + nfe, nfe);
              return false;
          }
  
          mHasSpeed = true;
          mSpeed = spd;
          return true;
      }
  
      public boolean parseSentence(String s) {
          int len = s.length();
          if (len < 9) {
              return false;
          }
          if (s.charAt(len - 3) == '*') {
              // String checksum = s.substring(len - 4, len);
              s = s.substring(0, len - 3);
          }
          String[] tokens = s.split(",");
          String sentenceId = tokens[0].substring(3, 6);
  
          int idx = 1;
          try {
              if (sentenceId.equals("GGA")) {
                  String time = tokens[idx++];
                  String latitude = tokens[idx++];
                  String latitudeHemi = tokens[idx++];
                  String longitude = tokens[idx++];
                  String longitudeHemi = tokens[idx++];
                  String fixQuality = tokens[idx++];
                  String numSatellites = tokens[idx++];
                  String horizontalDilutionOfPrecision = tokens[idx++];
                  String altitude = tokens[idx++];
                  String altitudeUnits = tokens[idx++];
                  String heightOfGeoid = tokens[idx++];
                  String heightOfGeoidUnits = tokens[idx++];
                  String timeSinceLastDgpsUpdate = tokens[idx++];
  
                  updateTime(time);
                  updateLatLon(latitude, latitudeHemi,
                          longitude, longitudeHemi);
                  updateAltitude(altitude);
                  // updateQuality(fixQuality);
                  updateIntExtra("numSatellites", numSatellites);
                  updateFloatExtra("hdop", horizontalDilutionOfPrecision);
  
                  if (mNewWaypoint) {
                      mNewWaypoint = false;
                      return true;
                  }
              } else if (sentenceId.equals("GSA")) {
                  // DOP and active satellites
                  String selectionMode = tokens[idx++]; // m=manual, a=auto 2d/3d
                  String mode = tokens[idx++]; // 1=no fix, 2=2d, 3=3d
                  for (int i = 0; i < 12; i++) {
                      String id = tokens[idx++];
                  }
                  String pdop = tokens[idx++];
                  String hdop = tokens[idx++];
                  String vdop = tokens[idx++];
  
                  // TODO - publish satellite ids
                  updateFloatExtra("pdop", pdop);
                  updateFloatExtra("hdop", hdop);
                  updateFloatExtra("vdop", vdop);
              } else if (sentenceId.equals("GSV")) {
                  // Satellites in view
                  String numMessages = tokens[idx++];
                  String messageNum = tokens[idx++];
                  String svsInView = tokens[idx++];
                  for (int i = 0; i < 4; i++) {
                      if (idx + 2 < tokens.length) {
                          String prnNumber = tokens[idx++];
                          String elevation = tokens[idx++];
                          String azimuth = tokens[idx++];
                          if (idx < tokens.length) {
                              String snr = tokens[idx++];
                          }
                      }
                  }
                  // TODO - publish this info
              } else if (sentenceId.equals("RMC")) {
                  // Recommended minimum navigation information
                  String time = tokens[idx++];
                  String fixStatus = tokens[idx++];
                  String latitude = tokens[idx++];
                  String latitudeHemi = tokens[idx++];
                  String longitude = tokens[idx++];
                  String longitudeHemi = tokens[idx++];
                  String speed = tokens[idx++];
                  String bearing = tokens[idx++];
                  String utcDate = tokens[idx++];
                  String magneticVariation = tokens[idx++];
                  String magneticVariationDir = tokens[idx++];
                  String mode = tokens[idx++];
  
                  if (fixStatus.charAt(0) == 'A') {
                      updateTime(time, utcDate);
                      updateLatLon(latitude, latitudeHemi,
                          longitude, longitudeHemi);
                      updateBearing(bearing);
                      updateSpeed(speed);
                  }
  
                  if (mNewWaypoint) {
                      return true;
                  }
              } else {
                  Log.e(TAG, "Unknown sentence: " + s);
              }
          } catch (ArrayIndexOutOfBoundsException e) {
              // do nothing - sentence will have no effect
              Log.e(TAG, "AIOOBE", e);
  
              for (int i = 0; i < tokens.length; i++) {
                  Log.e(TAG, "Got token #" + i + " = " + tokens[i]);
              }
          }
  
          return false;
      }
  
  //  } else if (sentenceId.equals("GLL")) {
  //  // Geographics position lat/long
  //  String latitude = tokens[idx++];
  //  String latitudeHemi = tokens[idx++];
  //  String longitude = tokens[idx++];
  //  String longitudeHemi = tokens[idx++];
  //  String time = tokens[idx++];
  //  String status = tokens[idx++];
  //  String mode = tokens[idx++];
  //  String checksum = tokens[idx++];
  //
  //  if (status.charAt(0) == 'A') {
  //      updateTime(time);
  //      updateLatLon(latitude, latitudeHemi, longitude, longitudeHemi);
  //  }
  //} else if (sentenceId.equals("VTG")) {
  //    String trackMadeGood = tokens[idx++];
  //    String t = tokens[idx++];
  //    String unused1 = tokens[idx++];
  //    String unused2 = tokens[idx++];
  //    String groundSpeedKnots = tokens[idx++];
  //    String n = tokens[idx++];
  //    String groundSpeedKph = tokens[idx++];
  //    String k = tokens[idx++];
  //    String checksum = tokens[idx++];
  //
  //    updateSpeed(groundSpeedKph);
  
      public Location getLocation() {
          return mLocation;
      }
  }
  