package com.wytehat.btlogger;

import android.app.*;
import android.bluetooth.*;
import android.content.*;
import android.graphics.*;
import android.location.*;
import android.net.*;
import android.os.*;
import android.view.*;
import android.webkit.*;
import android.widget.*;
import com.wytehat.btlogger.*;
import java.text.*;
import java.util.*;

public class MapActivity extends Activity
implements LocationListener, android.hardware.SensorEventListener,
MapHtmlBuilder.DataSource {

private TrackerDatabase db;  
private WebView web;  

private LocationManager locationManager;  
private android.hardware.SensorManager sensorManager;  
private android.hardware.Sensor rotationSensor;  

private Location phoneLocation;  
private float phoneHeading;  

private boolean mapReady;  

private LinearLayout filterPanel;  
private TextView filterHeader;  
private boolean filtersExpanded = false;  

private Spinner deviceSpinner;  
private Spinner categorySpinner;  

private Button startDateButton;  
private Button endDateButton;  
private Button startTimeButton;  
private Button endTimeButton;  

private CheckBox showHistory;  
private CheckBox showAll;  
private CheckBox showNormalized;
private CheckBox showChanges;

private SeekBar normalizationSeekBar;  
private TextView normalizationValueText;  
private TextView normalizationStatsText;  

private Button applyButton;  
private Button resetButton;  
private Button exportRawButton;  
private Button exportNormalizedButton;  

private final ArrayList<String> deviceValues =  
        new ArrayList<String>();  

private final ArrayList<String> deviceLabels =  
        new ArrayList<String>();  

private final ArrayList<String> categoryValues =  
        new ArrayList<String>();  

private final ArrayList<String> categoryLabels =  
        new ArrayList<String>();  

private ArrayAdapter<String> deviceAdapter;  
private ArrayAdapter<String> categoryAdapter;  

private boolean rebuildingDevices;  
private boolean rebuildingCategories;  

private String selectedDevice = "All";  
private String categoryFilter = "All";  

private Calendar filterStart;  
private Calendar filterEnd;  

private boolean normalizedMode;

/*
 * If true, the map also draws the points normalization removed.
 * Backs the "showChanges" checkbox, the same way normalizedMode
 * backs "showNormalized".
 */
private boolean showNormalizationChanges = true;

private final HistoryNormalizer normalizer =
        new HistoryNormalizer();

private MapExporter exporter;

private static final String[] CATEGORY_ORDER = {  
        "Trackers / Tags",  
        "Audio",  
        "Wearables",  
        "Phones",  
        "Computers",  
        "Vehicles",  
        "Other"  
};

@Override  
protected void onCreate(Bundle state) {  
    super.onCreate(state);  

    db = new TrackerDatabase(this);

    exporter =
            new MapExporter(
                    this,
                    db,
                    normalizer
            );

    android.content.SharedPreferences settings =  
            getSharedPreferences(  
                    "map_settings",  
                    0  
            );  

    normalizedMode =  
            settings.getBoolean(  
                    "normalized_mode",  
                    false  
            );  

    showNormalizationChanges =  
            settings.getBoolean(  
                    "show_normalization_changes",  
                    true  
            );  

    normalizer.setAggressiveness(
            settings.getInt(
                    "normalization_aggressiveness",
                    HistoryNormalizer.DEFAULT_AGGRESSIVENESS
            )
    );

    filterEnd = Calendar.getInstance();  

    filterStart = Calendar.getInstance();  

    filterStart.setTimeInMillis(  
            System.currentTimeMillis()  
                    - (60L * 60L * 1000L)  
    );  

    LinearLayout root =  
            new LinearLayout(this);  

    root.setOrientation(  
            LinearLayout.VERTICAL  
    );  

    root.setBackgroundColor(  
            Color.WHITE  
    );  

    root.addView(  
            NavigationHelper.createBar(  
                    this,  
                    2,  
                    "Tracked Item Map"  
            )  
    );  

    filterHeader =  
            new TextView(this);  

    filterHeader.setText(  
            "Map Filters  ▼"  
    );  

    filterHeader.setTextSize(16);  
    filterHeader.setTextColor(Color.DKGRAY);  

    filterHeader.setGravity(  
            Gravity.CENTER_VERTICAL  
    );  

    filterHeader.setPadding(  
            dp(14),  
            dp(10),  
            dp(14),  
            dp(10)  
    );  

    filterHeader.setBackgroundColor(  
            Color.rgb(238, 238, 238)  
    );  

    root.addView(  
            filterHeader,  
            new LinearLayout.LayoutParams(  
                    LinearLayout.LayoutParams.MATCH_PARENT,  
                    dp(48)  
            )  
    );  

    filterPanel =  
            new LinearLayout(this);  

    filterPanel.setOrientation(  
            LinearLayout.VERTICAL  
    );  

    filterPanel.setPadding(  
            dp(10),  
            dp(6),  
            dp(10),  
            dp(8)  
    );  

    filterPanel.setBackgroundColor(  
            Color.WHITE  
    );  

    buildFilterPanel();  

    root.addView(  
            filterPanel,  
            new LinearLayout.LayoutParams(  
                    LinearLayout.LayoutParams.MATCH_PARENT,  
                    LinearLayout.LayoutParams.WRAP_CONTENT  
            )  
    );  

    filterHeader.setOnClickListener(  
            new View.OnClickListener() {  
                @Override  
                public void onClick(View view) {  

                    setFiltersExpanded(  
                            !filtersExpanded  
                    );  
                }  
            }  
    );  

    setFiltersExpanded(false);  

    web = new WebView(this);  

    web.getSettings().setJavaScriptEnabled(true);  
    web.getSettings().setAllowFileAccess(true);  

    web.setWebViewClient(  
            new WebViewClient() {  

                @Override  
                public void onPageFinished(  
                        WebView view,  
                        String url) {  

                    mapReady = true;  
                    updatePhoneMarker();  
                }  

                @Override  
                public boolean shouldOverrideUrlLoading(  
                        WebView view,  
                        String url) {  

                    if (url != null &&  
                            url.startsWith("geo:")) {  

                        openMap(url);  
                        return true;  
                    }  

                    return false;  
                }  
            }  
    );  

    root.addView(  
            web,  
            new LinearLayout.LayoutParams(  
                    LinearLayout.LayoutParams.MATCH_PARENT,  
                    0,  
                    1  
            )  
    );  

    setContentView(root);  

    locationManager =  
            (LocationManager)  
                    getSystemService(  
                            LOCATION_SERVICE  
                    );  

    sensorManager =  
            (android.hardware.SensorManager)  
                    getSystemService(  
                            SENSOR_SERVICE  
                    );  

    if (sensorManager != null) {  

        rotationSensor =  
                sensorManager.getDefaultSensor(  
                        android.hardware.Sensor.TYPE_ROTATION_VECTOR  
                );  
    }  

    String focus =  
            getIntent().getStringExtra("focus");  

    if (focus != null &&  
            focus.trim().length() > 0) {  

        selectedDevice = focus;  
    }  

    reloadMap();  
}  

private int dp(int value) {  

    float density =  
            getResources()  
                    .getDisplayMetrics()  
                    .density;  

    return Math.round(  
            value * density  
    );  
}  

/*  
 * ================================================================  
 * FILTER UI  
 * ================================================================  
 */  

private void buildFilterPanel() {  

    TextView deviceTitle =  
            filterLabel("Tracked Device");  

    filterPanel.addView(deviceTitle);  

    deviceSpinner =  
            new Spinner(this);  

    deviceAdapter =  
            new ArrayAdapter<String>(  
                    this,  
                    android.R.layout.simple_spinner_item,  
                    deviceLabels  
            );  

    deviceAdapter.setDropDownViewResource(  
            android.R.layout.simple_spinner_dropdown_item  
    );  

    deviceSpinner.setAdapter(  
            deviceAdapter  
    );  

    filterPanel.addView(  
            deviceSpinner,  
            new LinearLayout.LayoutParams(  
                    -1,  
                    -2  
            )  
    );  

    deviceSpinner.setOnItemSelectedListener(  
            new android.widget.AdapterView.OnItemSelectedListener() {  

                @Override  
                public void onItemSelected(  
                        android.widget.AdapterView<?> parent,  
                        View view,  
                        int position,  
                        long id) {  

                    if (!rebuildingDevices &&  
                            position >= 0 &&  
                            position < deviceValues.size()) {  

                        selectedDevice =  
                                deviceValues.get(position);  
                    }  
                }  

                @Override  
                public void onNothingSelected(  
                        android.widget.AdapterView<?> parent) {  
                }  
            }  
    );  

    TextView categoryTitle =  
            filterLabel("Category");  

    filterPanel.addView(categoryTitle);  

    categorySpinner =  
            new Spinner(this);  

    categoryAdapter =  
            new ArrayAdapter<String>(  
                    this,  
                    android.R.layout.simple_spinner_item,  
                    categoryLabels  
            );  

    categoryAdapter.setDropDownViewResource(  
            android.R.layout.simple_spinner_dropdown_item  
    );  

    categorySpinner.setAdapter(  
            categoryAdapter  
    );  

    filterPanel.addView(  
            categorySpinner,  
            new LinearLayout.LayoutParams(  
                    -1,  
                    -2  
            )  
    );  

    categorySpinner.setOnItemSelectedListener(  
            new android.widget.AdapterView.OnItemSelectedListener() {  

                @Override  
                public void onItemSelected(  
                        android.widget.AdapterView<?> parent,  
                        View view,  
                        int position,  
                        long id) {  

                    if (!rebuildingCategories &&  
                            position >= 0 &&  
                            position < categoryValues.size()) {  

                        categoryFilter =  
                                categoryValues.get(position);  
                    }  
                }  

                @Override  
                public void onNothingSelected(  
                        android.widget.AdapterView<?> parent) {  
                }  
            }  
    );  

    TextView rangeTitle =  
            filterLabel("Date and Time Range");  

    filterPanel.addView(rangeTitle);  

    LinearLayout row1 =  
            new LinearLayout(this);  

    row1.setOrientation(  
            LinearLayout.HORIZONTAL  
    );  

    startDateButton =  
            makeFilterButton("Start Date");  

    endDateButton =  
            makeFilterButton("End Date");  

    row1.addView(  
            startDateButton,  
            weightParams()  
    );  

    row1.addView(  
            endDateButton,  
            weightParams()  
    );  

    filterPanel.addView(row1);  

    LinearLayout row2 =  
            new LinearLayout(this);  

    row2.setOrientation(  
            LinearLayout.HORIZONTAL  
    );  

    startTimeButton =  
            makeFilterButton("Start Time");  

    endTimeButton =  
            makeFilterButton("End Time");  

    row2.addView(  
            startTimeButton,  
            weightParams()  
    );  

    row2.addView(  
            endTimeButton,  
            weightParams()  
    );  

    filterPanel.addView(row2);  

    updateFilterButtonText();  

    startDateButton.setOnClickListener(  
            new View.OnClickListener() {  
                @Override  
                public void onClick(View v) {  
                    showDatePicker(true);  
                }  
            }  
    );  

    endDateButton.setOnClickListener(  
            new View.OnClickListener() {  
                @Override  
                public void onClick(View v) {  
                    showDatePicker(false);  
                }  
            }  
    );  

    startTimeButton.setOnClickListener(  
            new View.OnClickListener() {  
                @Override  
                public void onClick(View v) {  
                    showTimePicker(true);  
                }  
            }  
    );  

    endTimeButton.setOnClickListener(  
            new View.OnClickListener() {  
                @Override  
                public void onClick(View v) {  
                    showTimePicker(false);  
                }  
            }  
    );  

    showHistory =  
            new CheckBox(this);  

    showHistory.setText(  
            "Show previous recorded points"  
    );  

    showHistory.setChecked(true);  

    filterPanel.addView(showHistory);  

    showAll =  
            new CheckBox(this);  

    showAll.setText(  
            "Troubleshooting: show all scanned devices"  
    );  

    showAll.setChecked(  
            getSharedPreferences(  
                    "map_settings",  
                    0  
            ).getBoolean(  
                    "show_all_scanned",  
                    false  
            )  
    );  

    filterPanel.addView(showAll);  

    /*  
     * ============================================================  
     * NORMALIZATION SECTION  
     * ============================================================  
     */  

    TextView normalizationTitle =  
            filterLabel(  
                    "GPS Normalization"  
            );  

    normalizationTitle.setTextSize(16);  
    normalizationTitle.setTypeface(  
            null,  
            android.graphics.Typeface.BOLD  
    );  

    filterPanel.addView(  
            normalizationTitle  
    );  

    showNormalized =  
            new CheckBox(this);  

    showNormalized.setText(  
            "Apply normalization to map"  
    );  

    showNormalized.setChecked(  
            normalizedMode  
    );  

    filterPanel.addView(  
            showNormalized  
    );  

    showChanges =
            new CheckBox(this);

    showChanges.setText(
            "Show what normalization removed"
    );

    showChanges.setChecked(
            showNormalizationChanges
    );

    filterPanel.addView(
            showChanges
    );

    TextView aggressivenessLabel =  
            filterLabel(  
                    "Normalization aggressiveness"  
            );  

    filterPanel.addView(  
            aggressivenessLabel  
    );  

    normalizationSeekBar =  
            new SeekBar(this);  

    normalizationSeekBar.setMax(100);  

    normalizationSeekBar.setProgress(
            normalizer.getAggressiveness()
    );

    filterPanel.addView(  
            normalizationSeekBar,  
            new LinearLayout.LayoutParams(  
                    -1,  
                    dp(42)  
            )  
    );  

    normalizationValueText =  
            new TextView(this);  

    normalizationValueText.setTextSize(13);  
    normalizationValueText.setTextColor(  
            Color.DKGRAY  
    );  

    normalizationValueText.setPadding(  
            dp(4),  
            dp(0),  
            dp(4),  
            dp(6)  
    );  

    filterPanel.addView(  
            normalizationValueText  
    );  

    normalizationStatsText =  
            new TextView(this);  

    normalizationStatsText.setTextSize(12);  
    normalizationStatsText.setTextColor(  
            Color.GRAY  
    );  

    normalizationStatsText.setPadding(  
            dp(4),  
            dp(2),  
            dp(4),  
            dp(8)  
    );  

    filterPanel.addView(  
            normalizationStatsText  
    );  

    updateNormalizationControls();  

    normalizationSeekBar.setOnSeekBarChangeListener(  
            new SeekBar.OnSeekBarChangeListener() {  

                @Override  
                public void onProgressChanged(  
                        SeekBar seekBar,  
                        int progress,  
                        boolean fromUser) {  

                    normalizer.setAggressiveness(progress);

                    updateNormalizationControls();  

                    /*  
                     * Rebuild immediately so the user can drag  
                     * the slider and see the effect.  
                     */  
                    if (fromUser &&  
                            normalizedMode) {  

                        reloadMap();  
                    }  
                }  

                @Override  
                public void onStartTrackingTouch(  
                        SeekBar seekBar) {  
                }  

                @Override  
                public void onStopTrackingTouch(  
                        SeekBar seekBar) {  

                    saveNormalizationSettings();  

                    if (normalizedMode)  
                        reloadMap();  
                }  
            }  
    );  

    /*  
     * ============================================================  
     * NORMAL FILTER BUTTONS  
     * ============================================================  
     */  

    LinearLayout buttonRow =  
            new LinearLayout(this);  

    buttonRow.setOrientation(  
            LinearLayout.HORIZONTAL  
    );  

    applyButton =  
            new Button(this);  

    applyButton.setText(  
            "Apply Filters"  
    );  

    resetButton =  
            new Button(this);  

    resetButton.setText(  
            "Reset"  
    );  

    buttonRow.addView(  
            applyButton,  
            weightParams()  
    );  

    buttonRow.addView(  
            resetButton,  
            weightParams()  
    );  

    filterPanel.addView(buttonRow);  

    LinearLayout exportRow =  
            new LinearLayout(this);  

    exportRow.setOrientation(  
            LinearLayout.HORIZONTAL  
    );  

    exportRawButton =  
            new Button(this);  

    exportRawButton.setText(  
            "Export Raw"  
    );  

    exportNormalizedButton =  
            new Button(this);  

    exportNormalizedButton.setText(  
            "Export Normalized"  
    );  

    exportRow.addView(  
            exportRawButton,  
            weightParams()  
    );  

    exportRow.addView(  
            exportNormalizedButton,  
            weightParams()  
    );  

    filterPanel.addView(exportRow);  

    applyButton.setOnClickListener(  
            new View.OnClickListener() {  
                @Override  
                public void onClick(View v) {  

                    normalizeFilterRange();  

                    normalizedMode =  
                            showNormalized != null &&  
                                    showNormalized.isChecked();  

                    showNormalizationChanges =
                            showChanges != null &&
                                    showChanges.isChecked();

                    saveNormalizationSettings();  

                    reloadMap();  

                    setFiltersExpanded(false);  
                }  
            }  
    );  

    resetButton.setOnClickListener(  
            new View.OnClickListener() {  
                @Override  
                public void onClick(View v) {  

                    filterEnd =  
                            Calendar.getInstance();  

                    filterStart =  
                            Calendar.getInstance();  

                    filterStart.setTimeInMillis(  
                            filterEnd.getTimeInMillis()  
                                    -  
                                    (60L * 60L * 1000L)  
                    );  

                    selectedDevice = "All";  
                    categoryFilter = "All";  

                    normalizedMode = false;  

                    normalizer.setAggressiveness(
                            HistoryNormalizer.DEFAULT_AGGRESSIVENESS
                    );

                    showNormalizationChanges = true;  

                    if (showNormalized != null)  
                        showNormalized.setChecked(false);  

                    if (showChanges != null)
                        showChanges.setChecked(true);

                    if (normalizationSeekBar != null)
                        normalizationSeekBar.setProgress(
                                normalizer.getAggressiveness()
                        );

                    saveNormalizationSettings();  

                    updateNormalizationControls();  
                    updateFilterButtonText();  

                    reloadMap();  
                }  
            }  
    );  

    exportRawButton.setOnClickListener(  
            new View.OnClickListener() {  
                @Override  
                public void onClick(View v) {  

                    normalizeFilterRange();  

                    exportFilteredData(false);
                }  
            }  
    );  

    exportNormalizedButton.setOnClickListener(  
            new View.OnClickListener() {  
                @Override  
                public void onClick(View v) {  

                    normalizeFilterRange();  

                    exportFilteredData(true);
                }  
            }  
    );  

    showAll.setOnClickListener(  
            new View.OnClickListener() {  
                @Override  
                public void onClick(View view) {  

                    getSharedPreferences(  
                            "map_settings",  
                            0  
                    )  
                            .edit()  
                            .putBoolean(  
                                    "show_all_scanned",  
                                    showAll.isChecked()  
                            )  
                            .apply();  

                    reloadMap();  
                }  
            }  
    );  

    showNormalized.setOnClickListener(  
            new View.OnClickListener() {  
                @Override  
                public void onClick(View view) {  

                    normalizedMode =  
                            showNormalized.isChecked();  

                    saveNormalizationSettings();  

                    reloadMap();  
                }  
            }  
    );  

    showChanges.setOnClickListener(
            new View.OnClickListener() {  
                @Override  
                public void onClick(View view) {  

                    showNormalizationChanges =  
                            view instanceof CheckBox &&  
                                    ((CheckBox) view).isChecked();  

                    saveNormalizationSettings();  

                    if (normalizedMode)  
                        reloadMap();  
                }  
            }  
    );  
}  

/*  
 * ================================================================  
 * NORMALIZATION SETTINGS  
 * ================================================================  
 */  

private void saveNormalizationSettings() {  

    getSharedPreferences(  
            "map_settings",  
            0  
    )  
            .edit()  
            .putBoolean(  
                    "normalized_mode",  
                    normalizedMode  
            )  
            .putBoolean(  
                    "show_normalization_changes",  
                    showNormalizationChanges  
            )  
            .putInt(  
                    "normalization_aggressiveness",  
                    normalizer.getAggressiveness()
            )  
            .apply();  
}  

private void updateNormalizationControls() {

    if (normalizationValueText == null)
        return;

    normalizationValueText.setText(
            "Level: " +
                    normalizer.levelName() +
                    " (" +
                    normalizer.getAggressiveness() +
                    "%)"
    );

    normalizationStatsText.setText(
            "Current thresholds:\n" +
                    "Maximum movement speed: " +
                    MapFormat.number(
                            normalizer.maxSpeedKmh()
                    ) +
                    " km/h\n" +
                    "Duplicate tolerance: " +
                    MapFormat.number(
                            normalizer.duplicateDistance()
                    ) +
                    " m\n" +
                    "Same-time tolerance: " +
                    MapFormat.number(
                            normalizer.sameTimeDistance()
                    ) +
                    " m\n" +
                    "GPS accuracy multiplier: " +
                    MapFormat.number(
                            normalizer.accuracyMultiplier()
                    )
    );
}

/*  
 * ================================================================  
 * FILTER UI HELPERS  
 * ================================================================  
 */  

private void setFiltersExpanded(  
        boolean expanded) {  

    filtersExpanded = expanded;  

    filterPanel.setVisibility(  
            expanded  
                    ? View.VISIBLE  
                    : View.GONE  
    );  

    filterHeader.setText(  
            expanded  
                    ? "Map Filters  ▲"  
                    : "Map Filters  ▼"  
    );  
}  

private TextView filterLabel(  
        String text) {  

    TextView label =  
            new TextView(this);  

    label.setText(text);  
    label.setTextSize(14);  
    label.setTextColor(Color.DKGRAY);  

    label.setPadding(  
            dp(2),  
            dp(6),  
            dp(2),  
            dp(2)  
    );  

    return label;  
}  

private Button makeFilterButton(  
        String text) {  

    Button button =  
            new Button(this);  

    button.setText(text);  
    button.setTextSize(13);  

    return button;  
}  

private LinearLayout.LayoutParams weightParams() {  

    return new LinearLayout.LayoutParams(  
            0,  
            dp(48),  
            1  
    );  
}  

private void showDatePicker(  
        final boolean start) {  

    Calendar selected =  
            start  
                    ? filterStart  
                    : filterEnd;  

    DatePickerDialog dialog =  
            new DatePickerDialog(  
                    this,  
                    new DatePickerDialog.OnDateSetListener() {  

                        @Override  
                        public void onDateSet(  
                                DatePicker view,  
                                int year,  
                                int month,  
                                int day) {  

                            Calendar target =  
                                    start  
                                            ? filterStart  
                                            : filterEnd;  

                            target.set(  
                                    Calendar.YEAR,  
                                    year  
                            );  

                            target.set(  
                                    Calendar.MONTH,  
                                    month  
                            );  

                            target.set(  
                                    Calendar.DAY_OF_MONTH,  
                                    day  
                            );  

                            updateFilterButtonText();  
                        }  
                    },  
                    selected.get(Calendar.YEAR),  
                    selected.get(Calendar.MONTH),  
                    selected.get(Calendar.DAY_OF_MONTH)  
            );  

    dialog.show();  
}  

private void showTimePicker(  
        final boolean start) {  

    Calendar selected =  
            start  
                    ? filterStart  
                    : filterEnd;  

    TimePickerDialog dialog =  
            new TimePickerDialog(  
                    this,  
                    new TimePickerDialog.OnTimeSetListener() {  

                        @Override  
                        public void onTimeSet(  
                                TimePicker view,  
                                int hourOfDay,  
                                int minute) {  

                            Calendar target =  
                                    start  
                                            ? filterStart  
                                            : filterEnd;  

                            target.set(  
                                    Calendar.HOUR_OF_DAY,  
                                    hourOfDay  
                            );  

                            target.set(  
                                    Calendar.MINUTE,  
                                    minute  
                            );  

                            target.set(  
                                    Calendar.SECOND,  
                                    0  
                            );  

                            target.set(  
                                    Calendar.MILLISECOND,  
                                    0  
                            );  

                            updateFilterButtonText();  
                        }  
                    },  
                    selected.get(Calendar.HOUR_OF_DAY),  
                    selected.get(Calendar.MINUTE),  
                    false  
            );  

    dialog.show();  
}  

private void updateFilterButtonText() {  

    if (startDateButton == null)  
        return;  

    SimpleDateFormat dateFormat =  
            new SimpleDateFormat(  
                    "MMM d, yyyy",  
                    Locale.US  
            );  

    SimpleDateFormat timeFormat =  
            new SimpleDateFormat(  
                    "h:mm a",  
                    Locale.US  
            );  

    startDateButton.setText(  
            "From\n" +  
                    dateFormat.format(  
                            filterStart.getTime()  
                    )  
    );  

    endDateButton.setText(  
            "To\n" +  
                    dateFormat.format(  
                            filterEnd.getTime()  
                    )  
    );  

    startTimeButton.setText(  
            "From\n" +  
                    timeFormat.format(  
                            filterStart.getTime()  
                    )  
    );  

    endTimeButton.setText(  
            "To\n" +  
                    timeFormat.format(  
                            filterEnd.getTime()  
                    )  
    );  
}  

private void normalizeFilterRange() {  

    if (filterEnd.getTimeInMillis() <  
            filterStart.getTimeInMillis()) {  

        Calendar temp =  
                (Calendar)  
                        filterStart.clone();  

        filterStart =  
                (Calendar)  
                        filterEnd.clone();  

        filterEnd =  
                temp;  
    }  
}  

/*  
 * ================================================================  
 * MAP RELOAD  
 * ================================================================  
 */  

private void reloadMap() {

    mapReady = false;

    normalizer.clear();

    List<DeviceRecord> available =
            showAll != null &&
                    showAll.isChecked()
                    ? db.all()
                    : defaultMapItems();

    refreshDeviceFilter(available);
    refreshMapCategories(available);

    ArrayList<DeviceRecord> rows =
            new ArrayList<DeviceRecord>();

    for (int i = 0;
         i < available.size();
         i++) {

        DeviceRecord record =
                available.get(i);

        if (!record.hasLocation)
            continue;

        if (!"All".equals(selectedDevice) &&
                !deviceMatches(
                        record,
                        selectedDevice
                ))
            continue;

        if (!"All".equals(categoryFilter) &&
                !categoryFilter.equals(
                        record.category
                ))
            continue;

        rows.add(record);
    }

    if (normalizedMode) {

        normalizer.prepare(
                db,
                rows
        );

        updateNormalizationSummary();
    }

    String focus =
            "All".equals(selectedDevice)
                    ? null
                    : selectedDevice;

    web.loadDataWithBaseURL(
            "https://www.openstreetmap.org/",
            MapHtmlBuilder.build(
                    rows,
                    focus,
                    htmlOptions(),
                    this
            ),
            "text/html",
            "UTF-8",
            null
    );
}

private MapHtmlBuilder.Options htmlOptions() {

    MapHtmlBuilder.Options options =
            new MapHtmlBuilder.Options();

    options.normalizedMode = normalizedMode;
    options.showRemovedPoints = showNormalizationChanges;

    options.showHistory =
            showHistory != null &&
                    showHistory.isChecked();

    options.aggressiveness =
            normalizer.getAggressiveness();

    return options;
}

private void updateNormalizationSummary() {

    if (normalizationStatsText == null)
        return;

    normalizationStatsText.setText(
            "Level: " +
                    normalizer.levelName() +
                    " (" +
                    normalizer.getAggressiveness() +
                    "%)\n" +
                    "Maximum movement speed: " +
                    MapFormat.number(
                            normalizer.maxSpeedKmh()
                    ) +
                    " km/h\n" +
                    "Raw points examined: " +
                    normalizer.examinedCount() +
                    "\n" +
                    "Points retained: " +
                    normalizer.retainedCount() +
                    "\n" +
                    "Duplicates suppressed: " +
                    normalizer.duplicateCount() +
                    "\n" +
                    "GPS jumps rejected: " +
                    normalizer.rejectedCount()
    );
}

/*  
 * ================================================================  
 * DEVICE FILTERS  
 * ================================================================  
 */  

private void refreshDeviceFilter(  
        List<DeviceRecord> rows) {  

    ArrayList<String> nextValues =  
            new ArrayList<String>();  

    ArrayList<String> nextLabels =  
            new ArrayList<String>();  

    nextValues.add("All");  
    nextLabels.add("All Devices");  

    HashSet<String> seen =  
            new HashSet<String>();  

    for (int i = 0;  
         i < rows.size();  
         i++) {  

        DeviceRecord record =  
                rows.get(i);  

        if (record == null)  
            continue;  

        String value =  
                record.address;  

        if (value == null ||  
                value.trim().length() == 0)  
            continue;  

        if (seen.contains(value))  
            continue;  

        seen.add(value);  

        nextValues.add(value);  

        String label =  
                record.displayName();  

        if (label == null ||  
                label.trim().length() == 0)  
            label = value;  

        nextLabels.add(label);  
    }  

    if (!nextValues.contains(selectedDevice))  
        selectedDevice = "All";  

    rebuildingDevices = true;  

    deviceValues.clear();  
    deviceValues.addAll(nextValues);  

    deviceLabels.clear();  
    deviceLabels.addAll(nextLabels);  

    deviceAdapter.notifyDataSetChanged();  

    int position =  
            deviceValues.indexOf(  
                    selectedDevice  
            );  

    if (position < 0)  
        position = 0;  

    deviceSpinner.setSelection(  
            position  
    );  

    rebuildingDevices = false;  
}  

private boolean deviceMatches(  
        DeviceRecord record,  
        String selected) {  

    if (record == null)  
        return false;  

    if (selected == null ||  
            "All".equals(selected))  
        return true;  

    if (record.address != null &&  
            selected.equals(record.address))  
        return true;  

    String name =  
            record.displayName();  

    return name != null &&  
            selected.equals(name);  
}  

private void refreshMapCategories(  
        List<DeviceRecord> rows) {  

    int[] counts =  
            new int[CATEGORY_ORDER.length];  

    int located = 0;  

    for (int r = 0;  
         r < rows.size();  
         r++) {  

        DeviceRecord record =  
                rows.get(r);  

        if (!record.hasLocation)  
            continue;  

        located++;  

        for (int i = 0;  
             i < CATEGORY_ORDER.length;  
             i++) {  

            if (CATEGORY_ORDER[i].equals(  
                    record.category  
            )) {  

                counts[i]++;  
                break;  
            }  
        }  
    }  

    ArrayList<String> nextValues =  
            new ArrayList<String>();  

    ArrayList<String> nextLabels =  
            new ArrayList<String>();  

    nextValues.add("All");  

    nextLabels.add(  
            "All (" + located + ")"  
    );  

    for (int i = 0;  
         i < CATEGORY_ORDER.length;  
         i++) {  

        if (counts[i] > 0) {  

            nextValues.add(  
                    CATEGORY_ORDER[i]  
            );  

            nextLabels.add(  
                    CATEGORY_ORDER[i]  
                            + " ("  
                            + counts[i]  
                            + ")"  
            );  
        }  
    }  

    int selected =  
            nextValues.indexOf(  
                    categoryFilter  
            );  

    if (selected < 0) {  

        categoryFilter = "All";  
        selected = 0;  
    }  

    rebuildingCategories = true;  

    categoryValues.clear();  
    categoryValues.addAll(nextValues);  

    categoryLabels.clear();  
    categoryLabels.addAll(nextLabels);  

    categoryAdapter.notifyDataSetChanged();  

    categorySpinner.setSelection(  
            selected  
    );  

    rebuildingCategories = false;  
}  

private List<DeviceRecord> defaultMapItems() {  

    List<DeviceRecord> all =  
            db.all();  

    ArrayList<DeviceRecord> result =  
            new ArrayList<DeviceRecord>();  

    HashSet<String> paired =  
            new HashSet<String>();  

    try {  

        BluetoothAdapter adapter =  
                BluetoothAdapter.getDefaultAdapter();  

        if (adapter != null) {  

            for (BluetoothDevice device :  
                    adapter.getBondedDevices()) {  

                paired.add(  
                        device.getAddress()  
                );  
            }  
        }  

    } catch (Exception ignored) {  
    }  

    for (int i = 0;  
         i < all.size();  
         i++) {  

        DeviceRecord record =  
                all.get(i);  

        if (record.mapEnabled == 1)  
            result.add(record);  
    }  

    return result;  
}  

/*  
 * ================================================================  
 * NORMALIZED HISTORY  
 * ================================================================  
 */  

@Override
public List<LocationPoint> historyFor(
        DeviceRecord record) {

    if (record == null ||
            record.address == null)
        return new ArrayList<LocationPoint>();

    List<LocationPoint> history =
            normalizedMode
                    ? normalizer.historyOf(
                            db,
                            record.address
                    )
                    : db.history(
                            record.address,
                            0L
                    );

    return filterHistoryByDate(
            history
    );
}

@Override
public List<HistoryNormalizer.Decision> removedPointsFor(
        String address) {

    ArrayList<HistoryNormalizer.Decision> filtered =
            new ArrayList<HistoryNormalizer.Decision>();

    for (HistoryNormalizer.Decision decision :
            normalizer.decisionsOf(address)) {

        if (decision == null ||
                decision.point == null)
            continue;

        long time =
                HistoryNormalizer.pointTime(
                        decision.point
                );

        if (time <= 0L ||
                isInsideFilter(time)) {

            filtered.add(decision);
        }
    }

    return filtered;
}

private List<LocationPoint> filterHistoryByDate(  
        List<LocationPoint> history) {  

    ArrayList<LocationPoint> result =  
            new ArrayList<LocationPoint>();  

    if (history == null)  
        return result;  

    for (int i = 0;  
         i < history.size();  
         i++) {  

        LocationPoint point =  
                history.get(i);  

        if (point == null)  
            continue;  

        long time =
                HistoryNormalizer.pointTime(point);

        if (time <= 0L ||  
                isInsideFilter(time)) {  

            result.add(point);  
        }  
    }  

    return result;  
}  

private boolean isInsideFilter(  
        long time) {  

    if (time <= 0L)  
        return true;  

    return time >=  
            filterStart.getTimeInMillis()  
            &&  
            time <=  
                    filterEnd.getTimeInMillis();  
}  

/*  
 * ================================================================  
 * EXPORT  
 * ================================================================  
 */  

private void exportFilteredData(
        boolean normalized) {

    exporter.start(
            exportDevices(),
            normalized,
            currentFilters()
    );
}

/*
 * Every device the filter bar currently allows.
 *
 * Unlike the map, this keeps devices with no location fix -- their row
 * still carries battery, RSSI and status worth exporting.
 */
private List<DeviceRecord> exportDevices() {

    List<DeviceRecord> available =
            showAll != null &&
                    showAll.isChecked()
                    ? db.all()
                    : defaultMapItems();

    ArrayList<DeviceRecord> filtered =
            new ArrayList<DeviceRecord>();

    for (int i = 0;
         i < available.size();
         i++) {

        DeviceRecord record =
                available.get(i);

        if (record == null)
            continue;

        if (!"All".equals(selectedDevice) &&
                !deviceMatches(
                        record,
                        selectedDevice
                ))
            continue;

        if (!"All".equals(categoryFilter) &&
                !categoryFilter.equals(
                        record.category
                ))
            continue;

        filtered.add(record);
    }

    return filtered;
}

private MapExporter.Filters currentFilters() {

    MapExporter.Filters filters =
            new MapExporter.Filters();

    filters.device = selectedDevice;
    filters.category = categoryFilter;

    filters.startMillis =
            filterStart.getTimeInMillis();

    filters.endMillis =
            filterEnd.getTimeInMillis();

    filters.includeHistory =
            showHistory != null &&
                    showHistory.isChecked();

    return filters;
}

@Override
protected void onActivityResult(
        int requestCode,
        int resultCode,
        Intent data) {

    super.onActivityResult(
            requestCode,
            resultCode,
            data
    );

    if (resultCode != RESULT_OK ||
            data == null ||
            data.getData() == null) {

        return;
    }

    boolean normalized =
            requestCode ==
                    MapExporter.REQUEST_EXPORT_NORMALIZED;

    if (!normalized &&
            requestCode !=
                    MapExporter.REQUEST_EXPORT_RAW) {

        return;
    }

    exporter.write(
            data.getData(),
            exportDevices(),
            normalized,
            currentFilters()
    );
}

/*  
 * ================================================================  
 * PHONE LOCATION / SENSOR  
 * ================================================================  
 */  

@Override  
protected void onResume() {  

    super.onResume();  

    try {  

        Location gps =  
                locationManager == null  
                        ? null  
                        : locationManager  
                        .getLastKnownLocation(  
                                LocationManager.GPS_PROVIDER  
                        );  

        Location network =  
                locationManager == null  
                        ? null  
                        : locationManager  
                        .getLastKnownLocation(  
                                LocationManager.NETWORK_PROVIDER  
                        );  

        phoneLocation =  
                gps == null  
                        ? network  
                        : gps;  

        if (locationManager != null) {  

            locationManager.requestLocationUpdates(  
                    LocationManager.GPS_PROVIDER,  
                    1000L,  
                    1f,  
                    this  
            );  

            locationManager.requestLocationUpdates(  
                    LocationManager.NETWORK_PROVIDER,  
                    1000L,  
                    1f,  
                    this  
            );  
        }  

    } catch (Exception ignored) {  
    }  

    if (sensorManager != null &&  
            rotationSensor != null) {  

        sensorManager.registerListener(  
                this,  
                rotationSensor,  
                android.hardware.SensorManager.SENSOR_DELAY_UI  
        );  
    }  

    updatePhoneMarker();  
}  

@Override  
protected void onPause() {  

    try {  

        if (locationManager != null)  
            locationManager.removeUpdates(this);  

    } catch (Exception ignored) {  
    }  

    try {  

        if (sensorManager != null)  
            sensorManager.unregisterListener(this);  

    } catch (Exception ignored) {  
    }  

    super.onPause();  
}  

@Override  
public void onLocationChanged(  
        Location location) {  

    if (location == null)  
        return;  

    phoneLocation = location;  

    if (location.hasBearing() &&  
            location.hasSpeed() &&  
            location.getSpeed() > 0.8f) {  

        phoneHeading =  
                location.getBearing();  
    }  

    updatePhoneMarker();  
}  

/*  
 * Do NOT add onProviderEnabled() or onProviderDisabled().  
 * They caused the override compilation errors in the previous  
 * version.  
 */  
@Override  
@SuppressWarnings("deprecation")  
public void onStatusChanged(  
        String provider,  
        int status,  
        Bundle extras) {  
}  

@Override  
public void onSensorChanged(  
        android.hardware.SensorEvent event) {  

    if (event == null ||  
            event.sensor == null ||  
            event.sensor.getType() !=  
                    android.hardware.Sensor.TYPE_ROTATION_VECTOR)  
        return;  

    float[] rotation =  
            new float[9];  

    float[] orientation =  
            new float[3];  

    android.hardware.SensorManager  
            .getRotationMatrixFromVector(  
                    rotation,  
                    event.values  
            );  

    android.hardware.SensorManager  
            .getOrientation(  
                    rotation,  
                    orientation  
            );  

    float compass =  
            (float) Math.toDegrees(  
                    orientation[0]  
            );  

    if (compass < 0)  
        compass += 360f;  

    if (phoneLocation == null ||  
            !phoneLocation.hasSpeed() ||  
            phoneLocation.getSpeed() <= 0.8f) {  

        phoneHeading = compass;  
    }  

    updatePhoneMarker();  
}  

@Override  
public void onAccuracyChanged(  
        android.hardware.Sensor sensor,  
        int accuracy) {  
}  

private void updatePhoneMarker() {  

    if (!mapReady ||  
            web == null ||  
            phoneLocation == null)  
        return;  

    web.loadUrl(  
            String.format(  
                    Locale.US,  
                    "javascript:updatePhone(%.7f,%.7f,%.1f)",  
                    phoneLocation.getLatitude(),  
                    phoneLocation.getLongitude(),  
                    phoneHeading  
            )  
    );  
}  

/*  
 * ================================================================  
 * MISC  
 * ================================================================  
 */  

private void openMap(  
        String uri) {  

    Intent intent =  
            new Intent(  
                    Intent.ACTION_VIEW,  
                    Uri.parse(uri)  
            );  

    intent.setPackage(  
            "com.google.android.apps.maps"  
    );  

    try {  

        startActivity(intent);  

    } catch (Exception error) {  

        intent.setPackage(null);  

        try {  

            startActivity(intent);  

        } catch (Exception ignored) {  
        }  
    }  
}  

}