package cgeo.geocaching.ui.dialog;
import cgeo.geocaching.R;
import cgeo.geocaching.activity.AbstractActivity;
import cgeo.geocaching.activity.Keyboard;
import cgeo.geocaching.location.Geopoint;
import cgeo.geocaching.location.Geopoint.ParseException;
import cgeo.geocaching.location.GeopointFormatter;
import cgeo.geocaching.models.Geocache;
import cgeo.geocaching.sensors.Sensors;
import cgeo.geocaching.settings.Settings;
import cgeo.geocaching.settings.Settings.CoordInputFormatEnum;
import cgeo.geocaching.utils.ClipboardUtils;
import cgeo.geocaching.utils.EditUtils;
import android.app.Dialog;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnFocusChangeListener;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.Spinner;
import android.widget.TextView;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.List;
import butterknife.ButterKnife;
import org.apache.commons.lang3.StringUtils;
public class CoordinatesInputDialog extends DialogFragment {
private Geopoint gp;
private Geopoint cacheCoords;
private EditText eLat, eLon;
private Button bLat, bLon;
private EditText eLatDeg, eLatMin, eLatSec, eLatSub;
private EditText eLonDeg, eLonMin, eLonSec, eLonSub;
private TextView tLatSep1, tLatSep2, tLatSep3;
private TextView tLonSep1, tLonSep2, tLonSep3;
private CoordInputFormatEnum currentFormat = null;
private List<EditText> orderedInputs;
private static final String GEOPOINT_ARG = "GEOPOINT";
private static final String CACHECOORDS_ARG = "CACHECOORDS";
@NonNull
private static Geopoint currentCoords() {
return Sensors.getInstance().currentGeo().getCoords();
}
public static CoordinatesInputDialog getInstance(@Nullable final Geocache cache, @Nullable final Geopoint gp) {
final Bundle args = new Bundle();
if (gp != null) {
args.putParcelable(GEOPOINT_ARG, gp);
}
if (cache != null) {
args.putParcelable(CACHECOORDS_ARG, cache.getCoords());
}
final CoordinatesInputDialog cid = new CoordinatesInputDialog();
cid.setArguments(args);
return cid;
}
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
gp = getArguments().getParcelable(GEOPOINT_ARG);
if (gp == null) {
gp = currentCoords();
}
cacheCoords = getArguments().getParcelable(CACHECOORDS_ARG);
if (savedInstanceState != null && savedInstanceState.getParcelable(GEOPOINT_ARG) != null) {
gp = savedInstanceState.getParcelable(GEOPOINT_ARG);
}
}
@Override
public void onPause() {
super.onPause();
new Keyboard(getActivity()).hide();
}
@Override
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
// TODO: if current input is not committed in gp, read the current input into gp
outState.putParcelable(GEOPOINT_ARG, gp);
}
@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
final Dialog dialog = getDialog();
final boolean noTitle = dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
final View v = inflater.inflate(R.layout.coordinatesinput_dialog, container, false);
final InputDoneListener inputdone = new InputDoneListener();
if (!noTitle) {
dialog.setTitle(R.string.cache_coordinates);
} else {
final TextView title = ButterKnife.findById(v, R.id.dialog_title_title);
if (title != null) {
title.setText(R.string.cache_coordinates);
title.setVisibility(View.VISIBLE);
}
final ImageButton cancel = ButterKnife.findById(v, R.id.dialog_title_cancel);
if (cancel != null) {
cancel.setOnClickListener(new InputCancelListener());
cancel.setVisibility(View.VISIBLE);
}
final ImageButton done = ButterKnife.findById(v, R.id.dialog_title_done);
if (done != null) {
done.setOnClickListener(inputdone);
done.setVisibility(View.VISIBLE);
}
}
final Spinner spinner = ButterKnife.findById(v, R.id.spinnerCoordinateFormats);
final ArrayAdapter<CharSequence> adapter =
ArrayAdapter.createFromResource(getActivity(),
R.array.waypoint_coordinate_formats,
android.R.layout.simple_spinner_item);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
spinner.setSelection(Settings.getCoordInputFormat().ordinal());
spinner.setOnItemSelectedListener(new CoordinateFormatListener());
bLat = ButterKnife.findById(v, R.id.ButtonLat);
eLat = ButterKnife.findById(v, R.id.latitude);
eLatDeg = ButterKnife.findById(v, R.id.EditTextLatDeg);
eLatMin = ButterKnife.findById(v, R.id.EditTextLatMin);
eLatSec = ButterKnife.findById(v, R.id.EditTextLatSec);
eLatSub = ButterKnife.findById(v, R.id.EditTextLatSecFrac);
tLatSep1 = ButterKnife.findById(v, R.id.LatSeparator1);
tLatSep2 = ButterKnife.findById(v, R.id.LatSeparator2);
tLatSep3 = ButterKnife.findById(v, R.id.LatSeparator3);
bLon = ButterKnife.findById(v, R.id.ButtonLon);
eLon = ButterKnife.findById(v, R.id.longitude);
eLonDeg = ButterKnife.findById(v, R.id.EditTextLonDeg);
eLonMin = ButterKnife.findById(v, R.id.EditTextLonMin);
eLonSec = ButterKnife.findById(v, R.id.EditTextLonSec);
eLonSub = ButterKnife.findById(v, R.id.EditTextLonSecFrac);
tLonSep1 = ButterKnife.findById(v, R.id.LonSeparator1);
tLonSep2 = ButterKnife.findById(v, R.id.LonSeparator2);
tLonSep3 = ButterKnife.findById(v, R.id.LonSeparator3);
orderedInputs = Arrays.asList(eLatDeg, eLatMin, eLatSec, eLatSub, eLonDeg, eLonMin, eLonSec, eLonSub);
for (final EditText editText : orderedInputs) {
editText.addTextChangedListener(new SwitchToNextFieldWatcher(editText));
editText.setOnFocusChangeListener(new PadZerosOnFocusLostListener());
EditUtils.disableSuggestions(editText);
}
bLat.setOnClickListener(new ButtonClickListener());
bLon.setOnClickListener(new ButtonClickListener());
final Button buttonCurrent = ButterKnife.findById(v, R.id.current);
buttonCurrent.setOnClickListener(new CurrentListener());
final Button buttonCache = ButterKnife.findById(v, R.id.cache);
if (cacheCoords != null) {
buttonCache.setOnClickListener(new CacheListener());
} else {
buttonCache.setVisibility(View.GONE);
}
if (hasClipboardCoordinates()) {
final Button buttonClipboard = ButterKnife.findById(v, R.id.clipboard);
buttonClipboard.setOnClickListener(new ClipboardListener());
buttonClipboard.setVisibility(View.VISIBLE);
}
final Button buttonDone = ButterKnife.findById(v, R.id.done);
if (noTitle) {
buttonDone.setVisibility(View.GONE);
} else {
buttonDone.setOnClickListener(inputdone);
}
return v;
}
@SuppressWarnings("unused")
private static boolean hasClipboardCoordinates() {
try {
new Geopoint(StringUtils.defaultString(ClipboardUtils.getText()));
} catch (final ParseException ignored) {
return false;
}
return true;
}
private void updateGUI() {
if (gp == null) {
return;
}
bLat.setText(String.valueOf(gp.getLatDir()));
bLon.setText(String.valueOf(gp.getLonDir()));
switch (currentFormat) {
case Plain:
setVisible(R.id.coordTable, View.GONE);
eLat.setVisibility(View.VISIBLE);
eLon.setVisibility(View.VISIBLE);
eLat.setText(gp.format(GeopointFormatter.Format.LAT_DECMINUTE));
eLon.setText(gp.format(GeopointFormatter.Format.LON_DECMINUTE));
break;
case Deg: // DDD.DDDDD°
setVisible(R.id.coordTable, View.VISIBLE);
eLat.setVisibility(View.GONE);
eLon.setVisibility(View.GONE);
eLatSec.setVisibility(View.GONE);
eLonSec.setVisibility(View.GONE);
tLatSep3.setVisibility(View.GONE);
tLonSep3.setVisibility(View.GONE);
eLatSub.setVisibility(View.GONE);
eLonSub.setVisibility(View.GONE);
tLatSep1.setText(".");
tLonSep1.setText(".");
tLatSep2.setText("°");
tLonSep2.setText("°");
eLatDeg.setText(addZeros(gp.getLatDeg(), 2));
eLatMin.setText(addZeros(gp.getLatDegFrac(), 5));
eLatMin.setGravity(Gravity.NO_GRAVITY);
eLonDeg.setText(addZeros(gp.getLonDeg(), 3));
eLonMin.setText(addZeros(gp.getLonDegFrac(), 5));
eLonMin.setGravity(Gravity.NO_GRAVITY);
break;
case Min: // DDD° MM.MMM
setVisible(R.id.coordTable, View.VISIBLE);
eLat.setVisibility(View.GONE);
eLon.setVisibility(View.GONE);
eLatSec.setVisibility(View.VISIBLE);
eLonSec.setVisibility(View.VISIBLE);
tLatSep3.setVisibility(View.VISIBLE);
tLonSep3.setVisibility(View.VISIBLE);
eLatSub.setVisibility(View.GONE);
eLonSub.setVisibility(View.GONE);
tLatSep1.setText("°");
tLonSep1.setText("°");
tLatSep2.setText(".");
tLonSep2.setText(".");
tLatSep3.setText("'");
tLonSep3.setText("'");
eLatDeg.setText(addZeros(gp.getLatDeg(), 2));
eLatMin.setText(addZeros(gp.getLatMin(), 2));
eLatMin.setGravity(Gravity.RIGHT);
eLatSec.setText(addZeros(gp.getLatMinFrac(), 3));
eLonDeg.setText(addZeros(gp.getLonDeg(), 3));
eLonMin.setText(addZeros(gp.getLonMin(), 2));
eLonMin.setGravity(Gravity.RIGHT);
eLonSec.setText(addZeros(gp.getLonMinFrac(), 3));
break;
case Sec: // DDD° MM SS.SSS
setVisible(R.id.coordTable, View.VISIBLE);
eLat.setVisibility(View.GONE);
eLon.setVisibility(View.GONE);
eLatSec.setVisibility(View.VISIBLE);
eLonSec.setVisibility(View.VISIBLE);
tLatSep3.setVisibility(View.VISIBLE);
tLonSep3.setVisibility(View.VISIBLE);
eLatSub.setVisibility(View.VISIBLE);
eLonSub.setVisibility(View.VISIBLE);
tLatSep1.setText("°");
tLonSep1.setText("°");
tLatSep2.setText("'");
tLonSep2.setText("'");
tLatSep3.setText(".");
tLonSep3.setText(".");
eLatDeg.setText(addZeros(gp.getLatDeg(), 2));
eLatMin.setText(addZeros(gp.getLatMin(), 2));
eLatMin.setGravity(Gravity.RIGHT);
eLatSec.setText(addZeros(gp.getLatSec(), 2));
eLatSub.setText(addZeros(gp.getLatSecFrac(), 3));
eLonDeg.setText(addZeros(gp.getLonDeg(), 3));
eLonMin.setText(addZeros(gp.getLonMin(), 2));
eLonMin.setGravity(Gravity.RIGHT);
eLonSec.setText(addZeros(gp.getLonSec(), 2));
eLonSub.setText(addZeros(gp.getLonSecFrac(), 3));
break;
}
for (final EditText editText : orderedInputs) {
setSize(editText);
}
}
private void setVisible(@IdRes final int viewId, final int visibility) {
final View view = getView();
assert view != null;
ButterKnife.findById(view, viewId).setVisibility(visibility);
}
private void setSize(final EditText someEditText) {
if (someEditText.getVisibility() == View.GONE) {
return;
}
someEditText.setMinEms(getMaxLengthFromCurrentField(someEditText));
}
private static String addZeros(final int value, final int len) {
return StringUtils.leftPad(Integer.toString(value), len, '0');
}
private class ButtonClickListener implements View.OnClickListener {
@Override
public void onClick(final View view) {
final Button button = (Button) view;
final CharSequence text = button.getText();
if (StringUtils.isBlank(text)) {
return;
}
switch (text.charAt(0)) {
case 'N':
button.setText("S");
break;
case 'S':
button.setText("N");
break;
case 'E':
button.setText("W");
break;
case 'W':
button.setText("E");
break;
default:
break;
}
// This will serve as a reminder to the user that the current coordinates might not be valid
areCurrentCoordinatesValid(true);
}
}
private class SwitchToNextFieldWatcher implements TextWatcher {
/**
* weak reference, such that garbage collector can do its work
*/
private final WeakReference<EditText> editTextRef;
SwitchToNextFieldWatcher(final EditText editText) {
this.editTextRef = new WeakReference<>(editText);
}
@Override
public void afterTextChanged(final Editable s) {
if (currentFormat == CoordInputFormatEnum.Plain) {
return;
}
final EditText editText = editTextRef.get();
if (editText == null) {
return;
}
if (!editText.hasFocus()) {
return;
}
if (s.length() == getMaxLengthFromCurrentField(editText)) {
focusNextVisibleInput(editText);
}
}
private void focusNextVisibleInput(final EditText editText) {
int index = orderedInputs.indexOf(editText);
do {
index = (index + 1) % orderedInputs.size();
} while (orderedInputs.get(index).getVisibility() == View.GONE);
orderedInputs.get(index).requestFocus();
}
@Override
public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {
// nothing to do
}
@Override
public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
// nothing to do
}
}
private boolean areCurrentCoordinatesValid(final boolean signalError) {
try {
// first normalize all the text fields
final String latDir = bLat.getText().toString();
final String lonDir = bLon.getText().toString();
final String latDeg = eLatDeg.getText().toString();
final String lonDeg = eLonDeg.getText().toString();
// right-pad decimal fraction
final String latDegFrac = padZerosRight(eLatMin.getText().toString(), 5);
final String lonDegFrac = padZerosRight(eLonMin.getText().toString(), 5);
final String latMin = eLatMin.getText().toString();
final String lonMin = eLonMin.getText().toString();
final String latMinFrac = eLatSec.getText().toString();
final String lonMinFrac = eLonSec.getText().toString();
final String latSec = eLatSec.getText().toString();
final String lonSec = eLonSec.getText().toString();
// right-pad seconds fraction
final String latSecFrac = padZerosRight(eLatSub.getText().toString(), 3);
final String lonSecFrac = padZerosRight(eLonSub.getText().toString(), 3);
// then convert text to geopoint
final Geopoint current;
switch (currentFormat) {
case Deg:
current = new Geopoint(latDir, latDeg, latDegFrac, lonDir, lonDeg, lonDegFrac);
break;
case Min:
current = new Geopoint(latDir, latDeg, latMin, latMinFrac, lonDir, lonDeg, lonMin, lonMinFrac);
break;
case Sec:
current = new Geopoint(latDir, latDeg, latMin, latSec, latSecFrac, lonDir, lonDeg, lonMin, lonSec, lonSecFrac);
break;
case Plain:
current = new Geopoint(eLat.getText().toString(), eLon.getText().toString());
break;
default:
throw new IllegalStateException("can never happen, keep tools happy");
}
if (current.isValid()) {
gp = current;
return true;
}
} catch (final Geopoint.ParseException ignored) {
// Signaled and returned below
}
if (signalError) {
final AbstractActivity activity = (AbstractActivity) getActivity();
activity.showToast(activity.getString(R.string.err_parse_lat_lon));
}
return false;
}
private static String padZerosRight(final String value, final int len) {
return StringUtils.rightPad(value, len, '0');
}
/**
* Max lengths, depending on currentFormat
*
* formatPlain = disabled
* DEG MIN SEC SUB
* formatDeg 2/3 5 - -
* formatMin 2/3 2 3 -
* formatSec 2/3 2 2 3
*/
public int getMaxLengthFromCurrentField(final EditText editText) {
if (editText == eLonDeg || editText == eLatSub || editText == eLonSub) {
return 3;
}
if ((editText == eLatMin || editText == eLonMin) && currentFormat == CoordInputFormatEnum.Deg) {
return 5;
}
if ((editText == eLatSec || editText == eLonSec) && currentFormat == CoordInputFormatEnum.Min) {
return 3;
}
return 2;
}
private class CoordinateFormatListener implements OnItemSelectedListener {
@Override
public void onItemSelected(final AdapterView<?> parent, final View view, final int pos, final long id) {
// Ignore first call, which comes from onCreate()
if (currentFormat != null) {
// Start new format with an acceptable value: either the current one
// entered by the user, or our current position.
if (!areCurrentCoordinatesValid(false)) {
gp = currentCoords();
}
}
currentFormat = CoordInputFormatEnum.fromInt(pos);
Settings.setCoordInputFormat(currentFormat);
updateGUI();
// select first field
orderedInputs.get(0).requestFocus();
}
@Override
public void onNothingSelected(final AdapterView<?> arg0) {
// do nothing
}
}
private class CurrentListener implements View.OnClickListener {
@Override
public void onClick(final View v) {
gp = currentCoords();
updateGUI();
}
}
private class CacheListener implements View.OnClickListener {
@Override
public void onClick(final View v) {
if (cacheCoords == null) {
final AbstractActivity activity = (AbstractActivity) getActivity();
activity.showToast(activity.getString(R.string.err_location_unknown));
return;
}
gp = cacheCoords;
updateGUI();
}
}
private class ClipboardListener implements View.OnClickListener {
@Override
public void onClick(final View v) {
try {
gp = new Geopoint(StringUtils.defaultString(ClipboardUtils.getText()));
updateGUI();
} catch (final ParseException ignored) {
}
}
}
private class InputDoneListener implements View.OnClickListener {
@Override
public void onClick(final View v) {
if (!areCurrentCoordinatesValid(true)) {
return;
}
if (gp != null) {
((CoordinateUpdate) getActivity()).updateCoordinates(gp);
}
dismiss();
}
}
private class InputCancelListener implements View.OnClickListener {
@Override
public void onClick(final View v) {
dismiss();
}
}
public interface CoordinateUpdate {
void updateCoordinates(final Geopoint gp);
}
private class PadZerosOnFocusLostListener implements OnFocusChangeListener {
@Override
public void onFocusChange(final View v, final boolean hasFocus) {
if (!hasFocus) {
final EditText editText = (EditText) v;
final int maxLength = getMaxLengthFromCurrentField(editText);
if (editText.length() < maxLength) {
if ((editText.getGravity() & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.RIGHT) {
editText.setText(StringUtils.leftPad(editText.getText().toString(), maxLength, '0'));
} else {
editText.setText(StringUtils.rightPad(editText.getText().toString(), maxLength, '0'));
}
}
}
}
}
}