public class CarSelectionPreference extends DialogPreference {

  public static final String SENSOR_TYPE = "car";
  private static final Logger logger = Logger.getLogger(CarSelectionPreference.class);
  private static final String DEFAULT_VALUE = "null";
  private Car car;
  private LinearLayout garageProgress;
  private EditText modelEditText;
  private EditText manufacturerEditText;
  private EditText constructionYearEditText;
  protected String carModel;
  protected String carManufacturer;
  protected String carConstructionYear;
  protected String carFuelType;
  protected String carEngineDisplacement;

  private Spinner sensorSpinner;
  private ProgressBar sensorDlProgress;
  private Button sensorRetryButton;

  protected List<Car> sensors;
  private ScrollView garageForm;
  private RadioButton gasolineRadioButton;
  private RadioButton dieselRadioButton;
  private EditText engineDisplacementEditText;

  public CarSelectionPreference(Context context, AttributeSet attrs) {
    super(context, attrs);

    setDialogLayoutResource(R.layout.my_garage_layout);
    setPositiveButtonText(android.R.string.ok);
    setNegativeButtonText(android.R.string.cancel);

    setDialogIcon(null);
  }

  @Override
  protected void onBindDialogView(View view) {
    setupUIItems(view);
  }

  private void setupUIItems(View view) {
    // TODO !fancy! search for sensors
    garageForm = (ScrollView) view.findViewById(R.id.garage_form);
    garageProgress = (LinearLayout) view.findViewById(R.id.addCarToGarage_status);

    setupCarCreationItems(view);

    sensorSpinner = (Spinner) view.findViewById(R.id.dashboard_current_sensor_spinner);
    sensorSpinner.setOnItemSelectedListener(
        new OnItemSelectedListener() {
          private boolean firstSelect = true;

          @Override
          public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
            if (!firstSelect) {
              logger.info(parent.getItemAtPosition(pos) + "");

              updateCurrentSensor((Car) parent.getItemAtPosition(pos));
            } else {
              firstSelect = false;
            }
          }

          @Override
          public void onNothingSelected(AdapterView<?> parent) {
            logger.info("no change detected");
          }
        });
    sensorDlProgress = (ProgressBar) view.findViewById(R.id.sensor_dl_progress);
    sensorRetryButton = (Button) view.findViewById(R.id.retrybutton);
    sensorRetryButton.setOnClickListener(
        new OnClickListener() {
          @Override
          public void onClick(View view) {
            getCarList();
          }
        });

    getCarList();

    view.findViewById(R.id.mygaragelayout).requestFocus();
    view.findViewById(R.id.mygaragelayout).requestFocusFromTouch();
  }

  private void setupCarCreationItems(View view) {
    modelEditText = (EditText) view.findViewById(R.id.addCarToGarage_car_model);
    manufacturerEditText = (EditText) view.findViewById(R.id.addCarToGarage_car_manufacturer);
    constructionYearEditText =
        (EditText) view.findViewById(R.id.addCarToGarage_car_constructionYear);
    engineDisplacementEditText =
        (EditText) view.findViewById(R.id.addCarToGarage_car_engineDisplacement);

    TextWatcher textWatcher =
        new TextWatcher() {

          @Override
          public void onTextChanged(CharSequence s, int start, int before, int count) {
            carModel = modelEditText.getText().toString();
            carManufacturer = manufacturerEditText.getText().toString();
            carConstructionYear = constructionYearEditText.getText().toString();
            carEngineDisplacement = engineDisplacementEditText.getText().toString();
          }

          @Override
          public void afterTextChanged(Editable s) {}

          @Override
          public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
        };

    modelEditText.addTextChangedListener(textWatcher);
    manufacturerEditText.addTextChangedListener(textWatcher);
    constructionYearEditText.addTextChangedListener(textWatcher);
    engineDisplacementEditText.addTextChangedListener(textWatcher);

    OnClickListener listener =
        new OnClickListener() {
          @Override
          public void onClick(View v) {
            carFuelType = resolveFuelTypeFromCheckbox(v.getId());
            logger.info(carFuelType);
          }
        };

    RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.radiogroup_fueltype);
    carFuelType = resolveFuelTypeFromCheckbox(radioGroup.getCheckedRadioButtonId());

    gasolineRadioButton = (RadioButton) view.findViewById(R.id.radio_gasoline);
    gasolineRadioButton.setOnClickListener(listener);
    dieselRadioButton = (RadioButton) view.findViewById(R.id.radio_diesel);
    dieselRadioButton.setOnClickListener(listener);

    view.findViewById(R.id.register_car_button)
        .setOnClickListener(
            new View.OnClickListener() {
              @Override
              public void onClick(View view) {
                //						if(UserManager.instance().isLoggedIn()){
                registerSensorAtServer(
                    SENSOR_TYPE,
                    carManufacturer,
                    carModel,
                    carConstructionYear,
                    carFuelType,
                    carEngineDisplacement);
                //						}
                //						else {
                //							Toast.makeText(getDialog().getContext(),
                //									"Please log in", Toast.LENGTH_SHORT).show();
                //						}
              }
            });
  }

  /** Shows the progress UI and hides the register form. */
  @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
  private void showProgress(final boolean show) {
    // On Honeycomb MR2 we have the ViewPropertyAnimator APIs, which allow
    // for very easy animations. If available, use these APIs to fade-in
    // the progress spinner.
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
      int shortAnimTime =
          getContext().getResources().getInteger(android.R.integer.config_shortAnimTime);

      garageProgress.setVisibility(View.VISIBLE);
      garageProgress
          .animate()
          .setDuration(shortAnimTime)
          .alpha(show ? 1 : 0)
          .setListener(
              new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                  garageProgress.setVisibility(show ? View.VISIBLE : View.GONE);
                }
              });

      garageForm.setVisibility(View.VISIBLE);
      garageForm
          .animate()
          .setDuration(shortAnimTime)
          .alpha(show ? 0 : 1)
          .setListener(
              new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                  garageForm.setVisibility(show ? View.GONE : View.VISIBLE);
                }
              });
    } else {
      // The ViewPropertyAnimator APIs are not available, so simply show
      // and hide the relevant UI components.
      garageProgress.setVisibility(show ? View.VISIBLE : View.GONE);
      garageForm.setVisibility(show ? View.GONE : View.VISIBLE);
    }
  }

  /**
   * Register a new sensor (car) at the server
   *
   * @param sensorType
   * @param carManufacturer Car manufacturer
   * @param carModel Car model
   * @param carConstructionYear Construction year of the car
   * @param carFuelType Fuel type of the car
   */
  private void registerSensorAtServer(
      final String sensorType,
      final String carManufacturer,
      final String carModel,
      final String carConstructionYear,
      final String carFuelType,
      final String carEngineDisplacement) {

    try {
      checkEmpty(
          sensorType,
          carManufacturer,
          carModel,
          carConstructionYear,
          carConstructionYear,
          carFuelType,
          carEngineDisplacement);
    } catch (Exception e) {
      // TODO i18n
      Toast.makeText(getContext(), "Not all values were defined.", Toast.LENGTH_SHORT).show();
      return;
    }

    String sensorString =
        String.format(
            Locale.ENGLISH,
            "{ \"type\": \"%s\", \"properties\": {\"manufacturer\": \"%s\", \"model\": \"%s\", \"fuelType\": \"%s\", \"constructionYear\": %s, \"engineDisplacement\": %s } }",
            sensorType,
            carManufacturer,
            carModel,
            carFuelType,
            carConstructionYear,
            carEngineDisplacement);

    User user = UserManager.instance().getUser();
    String username = user.getUsername();
    String token = user.getToken();

    if (((SettingsActivity) getContext()).isConnectedToInternet()) {

      RestClient.createSensor(
          sensorString,
          username,
          token,
          new AsyncHttpResponseHandler() {

            @Override
            public void onStart() {
              super.onStart();
              showProgress(true);
            }

            @Override
            public void onFailure(Throwable error, String content) {
              super.onFailure(error, content);
              if (content != null && content.equals("can't resolve host")) {
                Toast.makeText(
                        getContext(),
                        getContext().getString(R.string.error_host_not_found),
                        Toast.LENGTH_SHORT)
                    .show();
              } else if (content != null && content.contains("Unauthorized")) {
                logger.info(
                    "Tried to register new car while not logged in. Creating temporary car.");
                Crouton.makeText(
                        (Activity) getContext(),
                        getContext().getResources().getString(R.string.creating_temp_car),
                        Style.INFO)
                    .show();
                createTemporaryCar();
              } else {
                logger.warn(
                    "Received error response: " + content + "; " + error.getMessage(), error);
                // TODO i18n
                Toast.makeText(getContext(), "Server Error: " + content, Toast.LENGTH_SHORT).show();
              }
              showProgress(false);
            }

            @Override
            public void onSuccess(int httpStatusCode, Header[] h, String response) {
              super.onSuccess(httpStatusCode, h, response);
              String location = "";
              for (int i = 0; i < h.length; i++) {
                if (h[i].getName().equals("Location")) {
                  location += h[i].getValue();
                  break;
                }
              }
              logger.info(httpStatusCode + " " + location);

              String sensorId =
                  location.substring(location.lastIndexOf("/") + 1, location.length());

              // put the sensor id into shared preferences
              int engineDisplacement = Integer.parseInt(carEngineDisplacement);
              int year = Integer.parseInt(carConstructionYear);
              car =
                  new Car(
                      Car.resolveFuelType(carFuelType),
                      carManufacturer,
                      carModel,
                      sensorId,
                      year,
                      engineDisplacement);
              persistCar();
            }
          });
    } else {
      createTemporaryCar();
    }
  }

  private void createTemporaryCar() {
    String sensorId = Car.TEMPORARY_SENSOR_ID + UUID.randomUUID().toString().substring(0, 5);
    createNewCar(sensorId);
  }

  private void createNewCar(String sensorId) {

    int year = Integer.parseInt(carConstructionYear);
    car =
        new Car(
            Car.resolveFuelType(carFuelType),
            carManufacturer,
            carModel,
            sensorId,
            year,
            Integer.parseInt(carEngineDisplacement));
    persistCar();
    Toast.makeText(
            getContext(), getContext().getString(R.string.creating_temp_car), Toast.LENGTH_SHORT)
        .show();
  }

  private void checkEmpty(String... values) throws Exception {
    for (String string : values) {
      if (string == null || string.isEmpty()) {
        throw new Exception("Empty value!");
      }
    }
  }

  /**
   * Get the fuel type form the checkbox
   *
   * @param resid
   * @return
   */
  private String resolveFuelTypeFromCheckbox(int resid) {
    switch (resid) {
      case R.id.radio_diesel:
        return FuelType.DIESEL.toString();
      case R.id.radio_gasoline:
        return FuelType.GASOLINE.toString();
    }
    return "none";
  }

  protected String downloadSensors() throws Exception {

    HttpGet getRequest = new HttpGet(ECApplication.BASE_URL + "/sensors");

    getRequest.addHeader("Accept-Encoding", "application/json");

    try {
      HttpResponse response = HTTPClient.execute(getRequest);

      String content = HTTPClient.readResponse(response.getEntity());

      JSONObject parentObject = new JSONObject(content);

      JSONArray res = (JSONArray) parentObject.get("sensors");

      addSensorsToList(res);

    } catch (Exception e) {
      logger.warn(e.getMessage(), e);
      throw e;
    }

    return "";
  }

  private void addSensorsToList(JSONArray res) {

    for (int i = 0; i < res.length(); i++) {
      String typeString;
      JSONObject properties;
      String carId;
      try {
        typeString = ((JSONObject) res.get(i)).optString("type", "none");
        properties = ((JSONObject) res.get(i)).getJSONObject("properties");
        carId = properties.getString("id");
      } catch (JSONException e) {
        logger.warn(e.getMessage(), e);
        continue;
      }
      if (typeString.equals(SENSOR_TYPE)) {
        try {
          sensors.add(Car.fromJsonWithStrictEngineDisplacement(properties));
        } catch (JSONException e) {
          logger.warn(
              String.format(
                  "Car '%s' not supported: %s", carId != null ? carId : "null", e.getMessage()));
        }
      }
    }

    SensorAdapter adapter = new SensorAdapter();
    sensorSpinner.setAdapter(adapter);
    int index = adapter.getInitialSelectedItem();
    sensorSpinner.setSelection(index);
  }

  public void getCarList() {

    sensorDlProgress.setVisibility(View.VISIBLE);
    sensorSpinner.setVisibility(View.GONE);
    sensorRetryButton.setVisibility(View.GONE);

    sensors = new ArrayList<Car>();

    if (((SettingsActivity) getContext()).isConnectedToInternet()) {
      try {
        new SensorDownloadTask().execute().get();
      } catch (Exception e) {
        logger.warn(e.getMessage(), e);
        Toast.makeText(getContext(), "Could not retrieve cars from server", Toast.LENGTH_SHORT)
            .show();
      }
      // TODO add possibility to update cache
      //			downloadSensors(true);
    } else {
      getCarsFromCache();
    }
    if (sensors.isEmpty()) {
      logger.warn("Got no cars neither from server nor from cache.");
      // TODO show warning that no cars were found i18n
      Toast.makeText(
              getContext(),
              "Could not retrieve cars from server or local cache",
              Toast.LENGTH_SHORT)
          .show();
    }
    sensorDlProgress.setVisibility(View.GONE);
    sensorSpinner.setVisibility(View.VISIBLE);
  }

  private void getCarsFromCache() {
    File directory;
    try {
      directory = Util.resolveExternalStorageBaseFolder();

      File f = new File(directory, CarManager.CAR_CACHE_FILE_NAME);

      if (f.isFile()) {
        BufferedReader bufferedReader = new BufferedReader(new FileReader(f));

        String content = "";
        String line = "";

        while ((line = bufferedReader.readLine()) != null) {
          content = content.concat(line);
        }

        bufferedReader.close();

        JSONArray cars = new JSONArray(content);

        addSensorsToList(cars);
      }
    } catch (IOException e) {
      logger.warn(e.getMessage(), e);
    } catch (JSONException e) {
      logger.warn(e.getMessage(), e);
    }
  }

  /**
   * This method updates the attributes of the current sensor (=car)
   *
   * @param sensorid the id that is stored on the server
   * @param carManufacturer the car manufacturer
   * @param carModel the car model
   * @param fuelType the fuel type of the car
   * @param year construction year of the car
   */
  private void updateCurrentSensor(Car car) {
    this.car = car;
  }

  @Override
  protected void onDialogClosed(boolean positiveResult) {
    if (positiveResult) {
      // this fixes issue #166
      persistCar();
    }
  }

  private void persistCar() {
    persistString(serializeCar(car));
    setSummary(car.toString());
    CarManager.instance().setCar(car);
  }

  @Override
  protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) {
    if (restorePersistedValue) {
      car = instantiateCar(this.getPersistedString(DEFAULT_VALUE));
    }

    if (car != null) {
      setSummary(car.toString());
    } else {
      setSummary(R.string.please_select);
    }
  }

  @Override
  protected Parcelable onSaveInstanceState() {
    final Parcelable superState = super.onSaveInstanceState();
    // Check whether this Preference is persistent (continually saved)
    if (isPersistent()) {
      // No need to save instance state since it's persistent, use superclass state
      return superState;
    }

    // Create instance of custom BaseSavedState
    final SavedState myState = new SavedState(superState);
    // Set the state's value with the class member that holds current setting value
    myState.car = car;
    return myState;
  }

  @Override
  protected void onRestoreInstanceState(Parcelable state) {
    // Check whether we saved the state in onSaveInstanceState
    if (state == null || !state.getClass().equals(SavedState.class)) {
      // Didn't save the state, so call superclass
      super.onRestoreInstanceState(state);
      return;
    }

    // Cast state to custom BaseSavedState and pass to superclass
    SavedState myState = (SavedState) state;
    super.onRestoreInstanceState(myState.getSuperState());
  }

  public static String serializeCar(Car car) {
    ObjectOutputStream oos = null;
    Base64OutputStream b64 = null;
    try {
      ByteArrayOutputStream byteArrayOut = new ByteArrayOutputStream();
      oos = new ObjectOutputStream(byteArrayOut);
      oos.writeObject(car);
      oos.flush();

      ByteArrayOutputStream out = new ByteArrayOutputStream();
      b64 = new Base64OutputStream(out, Base64.DEFAULT);
      b64.write(byteArrayOut.toByteArray());
      b64.flush();
      b64.close();
      out.flush();
      out.close();

      String result = new String(out.toByteArray());
      return result;
    } catch (IOException e) {
      logger.warn(e.getMessage(), e);
    } finally {
      if (oos != null)
        try {
          b64.close();
          oos.close();
        } catch (IOException e) {
          logger.warn(e.getMessage(), e);
        }
    }
    return null;
  }

  public static Car instantiateCar(String object) {
    if (object == null) return null;

    ObjectInputStream ois = null;
    try {
      Base64InputStream b64 =
          new Base64InputStream(new ByteArrayInputStream(object.getBytes()), Base64.DEFAULT);
      ois = new ObjectInputStream(b64);
      Car car = (Car) ois.readObject();
      return car;
    } catch (StreamCorruptedException e) {
      logger.warn(e.getMessage(), e);
    } catch (IOException e) {
      logger.warn(e.getMessage(), e);
    } catch (ClassNotFoundException e) {
      logger.warn(e.getMessage(), e);
    } finally {
      if (ois != null)
        try {
          ois.close();
        } catch (IOException e) {
          logger.warn(e.getMessage(), e);
        }
    }
    return null;
  }

  private class SensorDownloadTask extends AsyncTask<Void, String, String> {

    @Override
    protected String doInBackground(Void... params) {

      try {
        return downloadSensors();
      } catch (Exception e) {
        logger.warn(e.getMessage(), e);
      }
      return "";
    }
  }

  public static class SavedState extends BaseSavedState {
    // Member that holds the setting's value
    // Change this data type to match the type saved by your Preference
    Car car;

    public SavedState(Parcelable superState) {
      super(superState);
    }

    public SavedState(Parcel source) {
      super(source);
      // Get the current preference's value
      car = (Car) source.readSerializable(); // Change this to read the appropriate data type
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
      super.writeToParcel(dest, flags);
      // Write the preference's value
      dest.writeSerializable(car); // Change this to write the appropriate data type
    }

    // Standard creator object using an instance of this class
    public static final Parcelable.Creator<SavedState> CREATOR =
        new Parcelable.Creator<SavedState>() {

          public SavedState createFromParcel(Parcel in) {
            return new SavedState(in);
          }

          public SavedState[] newArray(int size) {
            return new SavedState[size];
          }
        };
  }

  private class SensorAdapter extends BaseAdapter implements SpinnerAdapter {

    @Override
    public int getCount() {
      return sensors.size() + 1;
    }

    public int getInitialSelectedItem() {
      if (car != null) {
        int index = 1;
        for (Car c : sensors) {
          if (c.equals(car)) {
            return index;
          }
          index++;
        }
      }
      return 0;
    }

    @Override
    public Object getItem(int position) {
      return sensors.get(position - 1);
    }

    @Override
    public long getItemId(int position) {
      return position;
    }

    @Override
    public View getView(int position, View view, ViewGroup parent) {
      TextView text;
      if (position == 0) {
        text = new TextView(parent.getContext());
        text.setText(getContext().getString(R.string.please_select));
      } else {
        text = new TextView(parent.getContext());
        text.setText(((Car) getItem(position)).toString());
      }

      return text;
    }
  }
}
Exemplo n.º 2
0
/**
 * This is a track. A track is a collection of measurements. All measurements of a track are
 * accessible via the track. The track stores meta information about the ride (car, fuel,
 * description...)
 */
public class Track implements Comparable<Track> {

  public enum TrackStatus {
    ONGOING {
      @Override
      public String toString() {
        return "ONGOING";
      }
    },

    FINISHED {
      @Override
      public String toString() {
        return "FINISHED";
      }
    }
  }

  private static final Logger logger = Logger.getLogger(Track.class);

  private long id;
  private String name;
  private String description;
  private List<Measurement> measurements = new ArrayList<Measurement>();
  private Car car;
  private AbstractConsumptionAlgorithm consumptionAlgorithm;
  private String remoteID;
  private Double consumptionPerHour;
  private TrackStatus status = TrackStatus.ONGOING;

  private DbAdapter dbAdapter;

  private boolean lazyLoadingMeasurements;

  private Long startTime = null;
  private Long endTime = null;

  private TrackMetadata metadata;

  public static Track createTrackWithId(long id, DbAdapter dbAdapterImpl) {
    Track track = new Track(id);
    track.dbAdapter = dbAdapterImpl;
    return track;
  }

  public static Track createNewLocalTrack(DbAdapter dbAdapterImpl) {
    Track t = new Track(dbAdapterImpl);
    return t;
  }

  public static Track createRemoteTrack(String remoteID, DbAdapter dbAdapter) {
    Track track = new Track(remoteID, dbAdapter);
    return track;
  }

  private Track(long id) {
    this.id = id;
  }

  private Track(String remoteID, DbAdapter dbAdapter) {
    this(dbAdapter);
    this.remoteID = remoteID;
    this.status = TrackStatus.FINISHED;
  }

  /**
   * Constructor for creating "fresh" new track. Use this for new measurements that were captured
   * from the OBD-II adapter.
   */
  private Track(DbAdapter dbAdapter) {
    this.name = "";
    this.description = "";
    this.measurements = new ArrayList<Measurement>();
    this.id = dbAdapter.insertTrack(this);
    this.dbAdapter = dbAdapter;
  }

  /** @return the localTrack */
  public boolean isLocalTrack() {
    return !isRemoteTrack();
  }

  public boolean isRemoteTrack() {
    return (remoteID != null ? true : false);
  }

  /** @return the name */
  public String getName() {
    return name;
  }

  /** @param name the name to set */
  public void setName(String name) {
    this.name = name;
  }

  /** @return the description */
  public String getDescription() {
    return description;
  }

  /** @param description the description to set */
  public void setDescription(String description) {
    this.description = description;
  }

  /** @return the measurements */
  public List<Measurement> getMeasurements() {
    if ((measurements == null || measurements.isEmpty()) && dbAdapter != null) {
      try {
        this.measurements = dbAdapter.getAllMeasurementsForTrack(this);
      } catch (TrackWithoutMeasurementsException e) {
        logger.warn(e.getMessage(), e);
      }
    }
    return measurements;
  }

  public Car getCar() {
    return car;
  }

  public void setCar(Car car) {
    this.car = car;
    this.consumptionAlgorithm = new BasicConsumptionAlgorithm(car);
  }

  /**
   * get the time where the track started
   *
   * @return start time of track as unix long
   * @throws MeasurementsException
   */
  public Long getStartTime() throws MeasurementsException {
    if (startTime != null) return startTime;

    if (this.getMeasurements().size() > 0) return this.getMeasurements().get(0).getTime();
    else throw new MeasurementsException("No measurements in the track");
  }

  public void setStartTime(Long time) {
    this.startTime = time;
  }

  /**
   * get the time where the track ended
   *
   * @return end time of track as unix long
   * @throws MeasurementsException
   */
  public Long getEndTime() throws MeasurementsException {
    if (endTime != null) return endTime;

    if (this.getMeasurements().size() > 0)
      return this.getMeasurements().get(this.getMeasurements().size() - 1).getTime();
    else throw new MeasurementsException("No measurements in the track");
  }

  public void setEndTime(Long time) {
    this.endTime = time;
  }

  /**
   * Sets the measurements with an arraylist of measurements
   *
   * @param measurements the measurements of a track
   */
  public void setMeasurementsAsArrayList(List<Measurement> measurements) {
    setMeasurementsAsArrayList(measurements, false);
  }

  public void setMeasurementsAsArrayList(List<Measurement> measurements, boolean storeInDb) {
    this.measurements = measurements;

    if (storeInDb) {
      storeMeasurementsInDbAdapter();
    }
  }

  private void storeMeasurementsInDbAdapter() {
    if (this.dbAdapter != null) {
      for (Measurement measurement : measurements) {
        try {
          this.dbAdapter.insertMeasurement(measurement);
        } catch (MeasurementsException e) {
          logger.warn(e.getMessage(), e);
        }
      }
    }
  }

  /**
   * Use this method only to insert "fresh" measurements, not to recreate a Track from the database
   * Use {@code insertMeasurement(ArrayList<Measurement> measurements)} instead Inserts measurments
   * into the Track and into the database!
   *
   * @param measurement
   * @throws TrackAlreadyFinishedException
   * @throws MeasurementsException
   */
  public void addMeasurement(Measurement measurement) throws TrackAlreadyFinishedException {
    measurement.setTrack(Track.this);
    this.measurements.add(measurement);
    if (this.dbAdapter != null) {
      try {
        this.dbAdapter.insertNewMeasurement(measurement);
      } catch (MeasurementsException e) {
        logger.severe("This should never happen", e);
        return;
      }
    } else {
      logger.warn("DbAdapter was null! Could not insert measurement");
    }
  }

  /**
   * Returns the number of measurements of this track
   *
   * @return
   */
  public int getNumberOfMeasurements() {
    return this.measurements.size();
  }

  /** @return the id */
  public long getId() {
    return id;
  }

  public String getRemoteID() {
    return remoteID;
  }

  public void setRemoteID(String remoteID) {
    this.remoteID = remoteID;
  }

  /**
   * Returns the length of a track in kilometers
   *
   * @return
   */
  public double getLengthOfTrack() {
    List<Measurement> measurements = this.getMeasurements();

    double distance = 0.0;

    if (measurements.size() > 1) {
      for (int i = 0; i < measurements.size() - 1; i++) {
        distance =
            distance
                + Util.getDistance(
                    measurements.get(i).getLatitude(),
                    measurements.get(i).getLongitude(),
                    measurements.get(i + 1).getLatitude(),
                    measurements.get(i + 1).getLongitude());
      }
    }
    return distance;
  }

  /**
   * Returns the last measurement of this track
   *
   * @return the last measurement or null if there are no measurements
   */
  public Measurement getLastMeasurement() {
    if (this.measurements.size() > 0) {
      return this.measurements.get(this.measurements.size() - 1);
    }
    return null;
  }

  /**
   * Returns the first measurement of this track
   *
   * @return Returns the last measurement or null if there are no measurements
   */
  public Measurement getFirstMeasurement() {
    if (this.measurements.size() > 0) {
      return this.measurements.get(0);
    }
    return null;
  }

  /**
   * Returns the average co2 emission for the track.
   *
   * @return
   */
  public double getCO2Average() {
    double co2Average = 0.0;
    try {
      for (Measurement measurement : measurements) {
        if (measurement.getProperty(CONSUMPTION) != null) {
          co2Average =
              co2Average
                  + consumptionAlgorithm.calculateCO2FromConsumption(
                      measurement.getProperty(CONSUMPTION));
        }
      }
      co2Average = co2Average / measurements.size();
    } catch (FuelConsumptionException e) {
      logger.warn(e.getMessage(), e);
    }
    return co2Average;
  }

  public double getFuelConsumptionPerHour() throws UnsupportedFuelTypeException {
    if (consumptionPerHour == null) {
      consumptionPerHour = 0.0;

      int consideredCount = 0;
      for (int i = 0; i < measurements.size(); i++) {
        try {
          consumptionPerHour =
              consumptionPerHour + consumptionAlgorithm.calculateConsumption(measurements.get(i));
          consideredCount++;
        } catch (FuelConsumptionException e) {
          logger.warn(e.getMessage());
        }
      }
      consumptionPerHour = consumptionPerHour / consideredCount;
    }
    return consumptionPerHour;
  }

  @Override
  public int compareTo(Track t) {
    try {
      if (t.getStartTime() == null && t.getEndTime() == null) {
        /*
         * we cannot assume any ordering
         */
        return 0;
      }
    } catch (MeasurementsException e) {
      return 0;
    }

    try {
      if (this.getStartTime() == null) {
        /*
         * no measurements, this is probably a relatively new track
         */
        return -1;
      }
    } catch (MeasurementsException e) {
      return -1;
    }

    try {
      if (t.getStartTime() == null) {
        /*
         * no measurements, that is probably a relatively new track
         */
        return 1;
      }
    } catch (MeasurementsException e) {
      return 1;
    }

    try {
      return (this.getStartTime() < t.getStartTime() ? 1 : -1);
    } catch (MeasurementsException e) {
      return 0;
    }
  }

  public double getLiterPerHundredKm() throws MeasurementsException {
    return consumptionPerHour * getDurationInMillis() / (1000 * 60 * 60) / getLengthOfTrack() * 100;
  }

  public long getDurationInMillis() throws MeasurementsException {
    return getEndTime() - getStartTime();
  }

  public double getGramsPerKm() throws FuelConsumptionException, MeasurementsException {

    if (this.car.getFuelType().equals(FuelType.GASOLINE)) {
      return getLiterPerHundredKm() * 23.3;
    } else if (this.car.getFuelType().equals(FuelType.DIESEL)) {
      return getLiterPerHundredKm() * 26.4;
    } else throw new FuelConsumptionException();
  }

  public void setStatus(TrackStatus s) {
    this.status = s;
  }

  public TrackStatus getStatus() {
    return status;
  }

  public void setLazyLoadingMeasurements(boolean b) {
    this.lazyLoadingMeasurements = b;
  }

  public boolean isLazyLoadingMeasurements() {
    return lazyLoadingMeasurements;
  }

  /**
   * Creates a Track and adds its contents to the DB layer (adapter).
   *
   * @param json the input json object
   * @param adapter the DB layer adapter
   * @return the Track object
   * @throws JSONException parsing fails or contains unexpected properties
   * @throws ParseException if DateTime parsing fails
   */
  public static Track fromJson(JSONObject json, DbAdapter adapter)
      throws JSONException, ParseException {
    JSONObject trackProperties = json.getJSONObject("properties");
    Track t = Track.createRemoteTrack(trackProperties.getString("id"), adapter);
    String trackName = "unnamed Track #" + t.getId();
    try {
      trackName = trackProperties.getString("name");
    } catch (JSONException e) {
      logger.warn(e.getMessage(), e);
    }

    t.setName(trackName);
    String description = "";
    try {
      description = trackProperties.getString("description");
    } catch (JSONException e) {
      logger.warn(e.getMessage(), e);
    }

    t.setDescription(description);
    JSONObject sensorProperties =
        trackProperties.getJSONObject("sensor").getJSONObject("properties");

    t.setCar(Car.fromJson(sensorProperties));
    // include server properties tracks created, modified?

    t.dbAdapter.updateTrack(t);
    // Log.i("track_id",t.getId()+" "+((DbAdapterRemote)
    // dbAdapter).trackExistsInDatabase(t.getId())+" "+dbAdapter.getNumberOfStoredTracks());

    Measurement recycleMeasurement;

    List<Measurement> measurements = new ArrayList<Measurement>();
    JSONArray features = json.getJSONArray("features");
    logger.info(
        "Parsing measurements of track " + t.getRemoteID() + ". Count: " + features.length());
    for (int j = 0; j < features.length(); j++) {
      JSONObject measurementJsonObject = features.getJSONObject(j);
      recycleMeasurement = Measurement.fromJson(measurementJsonObject);

      recycleMeasurement.setTrack(t);
      measurements.add(recycleMeasurement);
    }

    t.setMeasurementsAsArrayList(measurements);
    logger.info("Storing measurements in database");
    t.storeMeasurementsInDbAdapter();
    return t;
  }

  public boolean isFinished() {
    return status != null && status == TrackStatus.FINISHED;
  }

  /**
   * updates the tracks metadata. if there is already metadata, the properties are merged. the
   * provided object overrides existing keys.
   *
   * @param newMetadata
   */
  public void updateMetadata(TrackMetadata newMetadata) {
    if (this.metadata != null) {
      this.metadata.merge(newMetadata);
    } else {
      setMetadata(newMetadata);
    }

    dbAdapter.updateTrack(this);
  }

  public TrackMetadata getMetadata() {
    return this.metadata;
  }

  public void setMetadata(TrackMetadata m) {
    this.metadata = m;
  }

  @Override
  public String toString() {
    return "Track / id: " + getId() + " / Name: " + getName();
  }
}