feat(map): add GPS point downsampling and UI density control
This commit is contained in:
@@ -16,6 +16,8 @@ import android.location.LocationListener;
|
|||||||
import android.location.LocationManager;
|
import android.location.LocationManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.webkit.WebView;
|
import android.webkit.WebView;
|
||||||
import android.webkit.WebViewClient;
|
import android.webkit.WebViewClient;
|
||||||
@@ -34,6 +36,8 @@ import java.util.Date;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
public class MapActivity extends Activity
|
public class MapActivity extends Activity
|
||||||
implements SensorEventListener {
|
implements SensorEventListener {
|
||||||
@@ -52,6 +56,7 @@ implements SensorEventListener {
|
|||||||
|
|
||||||
private Spinner categorySpinner;
|
private Spinner categorySpinner;
|
||||||
private Spinner deviceSpinner;
|
private Spinner deviceSpinner;
|
||||||
|
private Spinner resolutionSpinner;
|
||||||
|
|
||||||
private CheckBox showAll;
|
private CheckBox showAll;
|
||||||
|
|
||||||
@@ -70,15 +75,54 @@ implements SensorEventListener {
|
|||||||
private final ArrayList<String> deviceLabels =
|
private final ArrayList<String> deviceLabels =
|
||||||
new ArrayList<String>();
|
new ArrayList<String>();
|
||||||
|
|
||||||
private String categoryFilter = "Trackers / Tags";
|
private final ArrayList<String> resolutionLabels =
|
||||||
private String deviceFilter = "All";
|
new ArrayList<String>();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Time-bucket widths for the trail sampler.
|
||||||
|
* 0 = every stored point (still capped by TrackPointSampler).
|
||||||
|
*/
|
||||||
|
private static final long[] RESOLUTION_VALUES = {
|
||||||
|
0L,
|
||||||
|
15L * 60L * 1000L,
|
||||||
|
60L * 60L * 1000L,
|
||||||
|
120L * 60L * 1000L
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final String[] RESOLUTION_LABELS = {
|
||||||
|
"All points",
|
||||||
|
"Every 15 min",
|
||||||
|
"Every 1 hour",
|
||||||
|
"Every 2 hours"
|
||||||
|
};
|
||||||
|
|
||||||
|
private volatile String categoryFilter = "Trackers / Tags";
|
||||||
|
private volatile String deviceFilter = "All";
|
||||||
|
|
||||||
private boolean rebuildingCategories;
|
private boolean rebuildingCategories;
|
||||||
private boolean rebuildingDevices;
|
private boolean rebuildingDevices;
|
||||||
private boolean loadingMap;
|
private boolean rebuildingResolution = true;
|
||||||
|
private volatile boolean loadingMap;
|
||||||
|
|
||||||
private long historyFrom;
|
private volatile long historyFrom;
|
||||||
private long historyTo;
|
private volatile long historyTo;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Selected trail resolution, read by the background loader.
|
||||||
|
*/
|
||||||
|
private volatile long sampleInterval = RESOLUTION_VALUES[0];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Map data (SQLite reads + HTML generation) is built off the main
|
||||||
|
* thread; only the WebView load and the spinner refresh run on it.
|
||||||
|
*/
|
||||||
|
private final ExecutorService mapExecutor =
|
||||||
|
Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
|
private final Handler ui =
|
||||||
|
new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
|
private volatile boolean destroyed;
|
||||||
|
|
||||||
private LocationManager locationManager;
|
private LocationManager locationManager;
|
||||||
private SensorManager sensorManager;
|
private SensorManager sensorManager;
|
||||||
@@ -274,6 +318,45 @@ implements SensorEventListener {
|
|||||||
compactParams()
|
compactParams()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Trail resolution (downsampling).
|
||||||
|
*/
|
||||||
|
TextView resolutionLabel =
|
||||||
|
smallLabel("Trail detail");
|
||||||
|
|
||||||
|
filterPanel.addView(resolutionLabel);
|
||||||
|
|
||||||
|
resolutionSpinner = new Spinner(this);
|
||||||
|
|
||||||
|
for (int i = 0;
|
||||||
|
i < RESOLUTION_LABELS.length;
|
||||||
|
i++) {
|
||||||
|
|
||||||
|
resolutionLabels.add(
|
||||||
|
RESOLUTION_LABELS[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayAdapter<String> resolutionAdapter =
|
||||||
|
new ArrayAdapter<String>(
|
||||||
|
this,
|
||||||
|
android.R.layout.simple_spinner_item,
|
||||||
|
resolutionLabels
|
||||||
|
);
|
||||||
|
|
||||||
|
resolutionAdapter.setDropDownViewResource(
|
||||||
|
android.R.layout.simple_spinner_dropdown_item
|
||||||
|
);
|
||||||
|
|
||||||
|
resolutionSpinner.setAdapter(
|
||||||
|
resolutionAdapter
|
||||||
|
);
|
||||||
|
|
||||||
|
filterPanel.addView(
|
||||||
|
resolutionSpinner,
|
||||||
|
compactParams()
|
||||||
|
);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Time.
|
* Time.
|
||||||
*/
|
*/
|
||||||
@@ -567,6 +650,50 @@ implements SensorEventListener {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Trail resolution: applied immediately so the user can compare
|
||||||
|
* densities without reopening the panel.
|
||||||
|
*/
|
||||||
|
resolutionSpinner.setOnItemSelectedListener(
|
||||||
|
new AdapterView.OnItemSelectedListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemSelected(
|
||||||
|
AdapterView<?> parent,
|
||||||
|
View view,
|
||||||
|
int position,
|
||||||
|
long id) {
|
||||||
|
|
||||||
|
if (rebuildingResolution) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (position < 0 ||
|
||||||
|
position >=
|
||||||
|
RESOLUTION_VALUES.length) {
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sampleInterval ==
|
||||||
|
RESOLUTION_VALUES[position]) {
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sampleInterval =
|
||||||
|
RESOLUTION_VALUES[position];
|
||||||
|
|
||||||
|
reloadMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNothingSelected(
|
||||||
|
AdapterView<?> parent) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
fromButton.setOnClickListener(
|
fromButton.setOnClickListener(
|
||||||
new View.OnClickListener() {
|
new View.OnClickListener() {
|
||||||
|
|
||||||
@@ -681,6 +808,8 @@ implements SensorEventListener {
|
|||||||
|
|
||||||
updateTimeButtons();
|
updateTimeButtons();
|
||||||
|
|
||||||
|
rebuildingResolution = false;
|
||||||
|
|
||||||
rebuildDeviceFilter();
|
rebuildDeviceFilter();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -893,8 +1022,24 @@ implements SensorEventListener {
|
|||||||
|
|
||||||
private void rebuildDeviceFilter() {
|
private void rebuildDeviceFilter() {
|
||||||
|
|
||||||
List<DeviceRecord> available =
|
rebuildDeviceFilter(
|
||||||
getMapDevices();
|
getMapDevices(
|
||||||
|
showAll.isChecked()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Main thread only. Accepts an already loaded device list so the
|
||||||
|
* background loader does not repeat the query.
|
||||||
|
*/
|
||||||
|
private void rebuildDeviceFilter(
|
||||||
|
List<DeviceRecord> available) {
|
||||||
|
|
||||||
|
if (available == null) {
|
||||||
|
available =
|
||||||
|
new ArrayList<DeviceRecord>();
|
||||||
|
}
|
||||||
|
|
||||||
ArrayList<String> nextAddresses =
|
ArrayList<String> nextAddresses =
|
||||||
new ArrayList<String>();
|
new ArrayList<String>();
|
||||||
@@ -993,10 +1138,19 @@ implements SensorEventListener {
|
|||||||
rebuildingDevices = false;
|
rebuildingDevices = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<DeviceRecord> getMapDevices() {
|
/*
|
||||||
|
* Safe to call from a worker thread: the caller passes the checkbox
|
||||||
|
* state in instead of reading the view here.
|
||||||
|
*/
|
||||||
|
private List<DeviceRecord> getMapDevices(
|
||||||
|
boolean includeAll) {
|
||||||
|
|
||||||
|
TrackerDatabase database = db;
|
||||||
|
|
||||||
List<DeviceRecord> all =
|
List<DeviceRecord> all =
|
||||||
db.all();
|
database == null
|
||||||
|
? null
|
||||||
|
: database.all();
|
||||||
|
|
||||||
ArrayList<DeviceRecord> result =
|
ArrayList<DeviceRecord> result =
|
||||||
new ArrayList<DeviceRecord>();
|
new ArrayList<DeviceRecord>();
|
||||||
@@ -1020,7 +1174,7 @@ implements SensorEventListener {
|
|||||||
* Normal map:
|
* Normal map:
|
||||||
* only explicitly map-enabled devices.
|
* only explicitly map-enabled devices.
|
||||||
*/
|
*/
|
||||||
if (!showAll.isChecked() &&
|
if (!includeAll &&
|
||||||
record.mapEnabled != 1) {
|
record.mapEnabled != 1) {
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
@@ -1029,7 +1183,7 @@ implements SensorEventListener {
|
|||||||
/*
|
/*
|
||||||
* Normal map excludes unknown entries.
|
* Normal map excludes unknown entries.
|
||||||
*/
|
*/
|
||||||
if (!showAll.isChecked() &&
|
if (!includeAll &&
|
||||||
isUnknown(record)) {
|
isUnknown(record)) {
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
@@ -1220,20 +1374,42 @@ implements SensorEventListener {
|
|||||||
loadingMap = true;
|
loadingMap = true;
|
||||||
mapReady = false;
|
mapReady = false;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* View state is snapshotted on the main thread; everything after
|
||||||
|
* this point (SQLite reads, trail sampling, HTML generation) runs
|
||||||
|
* on the worker.
|
||||||
|
*/
|
||||||
|
final boolean includeAll =
|
||||||
|
showAll.isChecked();
|
||||||
|
|
||||||
|
mapExecutor.execute(
|
||||||
|
new Runnable() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
|
||||||
|
List<DeviceRecord> available = null;
|
||||||
|
String html = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
List<DeviceRecord> available =
|
available =
|
||||||
getMapDevices();
|
getMapDevices(includeAll);
|
||||||
|
|
||||||
refreshMapCategories(
|
String device = deviceFilter;
|
||||||
available
|
|
||||||
);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Rebuild device list after category
|
* The selected device may have gone away since
|
||||||
* changes.
|
* the last load; fall back to showing all.
|
||||||
*/
|
*/
|
||||||
rebuildDeviceFilter();
|
if (!"All".equals(device) &&
|
||||||
|
!containsAddress(
|
||||||
|
available,
|
||||||
|
device
|
||||||
|
)) {
|
||||||
|
|
||||||
|
device = "All";
|
||||||
|
}
|
||||||
|
|
||||||
ArrayList<DeviceRecord> rows =
|
ArrayList<DeviceRecord> rows =
|
||||||
new ArrayList<DeviceRecord>();
|
new ArrayList<DeviceRecord>();
|
||||||
@@ -1254,8 +1430,8 @@ implements SensorEventListener {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!"All".equals(deviceFilter) &&
|
if (!"All".equals(device) &&
|
||||||
!deviceFilter.equals(
|
!device.equals(
|
||||||
record.address
|
record.address
|
||||||
)) {
|
)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -1264,29 +1440,100 @@ implements SensorEventListener {
|
|||||||
rows.add(record);
|
rows.add(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
String html =
|
html =
|
||||||
buildHtml(
|
buildHtml(
|
||||||
rows,
|
rows,
|
||||||
focus
|
focus,
|
||||||
|
device
|
||||||
);
|
);
|
||||||
|
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<DeviceRecord> loaded =
|
||||||
|
available;
|
||||||
|
|
||||||
|
final String content = html;
|
||||||
|
|
||||||
|
ui.post(
|
||||||
|
new Runnable() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
if (destroyed ||
|
||||||
|
web == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshMapCategories(
|
||||||
|
loaded
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Rebuild device list after
|
||||||
|
* category changes.
|
||||||
|
*/
|
||||||
|
rebuildDeviceFilter(
|
||||||
|
loaded
|
||||||
|
);
|
||||||
|
|
||||||
|
if (content != null) {
|
||||||
|
|
||||||
web.loadDataWithBaseURL(
|
web.loadDataWithBaseURL(
|
||||||
"https://unpkg.com/",
|
"https://unpkg.com/",
|
||||||
html,
|
content,
|
||||||
"text/html",
|
"text/html",
|
||||||
"UTF-8",
|
"UTF-8",
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
|
|
||||||
loadingMap = false;
|
loadingMap = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean containsAddress(
|
||||||
|
List<DeviceRecord> records,
|
||||||
|
String address) {
|
||||||
|
|
||||||
|
if (records == null ||
|
||||||
|
address == null) {
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0;
|
||||||
|
i < records.size();
|
||||||
|
i++) {
|
||||||
|
|
||||||
|
DeviceRecord record =
|
||||||
|
records.get(i);
|
||||||
|
|
||||||
|
if (record != null &&
|
||||||
|
address.equals(record.address)) {
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private String buildHtml(
|
private String buildHtml(
|
||||||
List<DeviceRecord> rows,
|
List<DeviceRecord> rows,
|
||||||
String selectedAddress) {
|
String selectedAddress,
|
||||||
|
String device) {
|
||||||
|
|
||||||
StringBuilder script =
|
StringBuilder script =
|
||||||
new StringBuilder();
|
new StringBuilder();
|
||||||
@@ -1295,7 +1542,7 @@ implements SensorEventListener {
|
|||||||
* Historical data is loaded ONLY for a
|
* Historical data is loaded ONLY for a
|
||||||
* specifically selected device.
|
* specifically selected device.
|
||||||
*/
|
*/
|
||||||
if (!"All".equals(deviceFilter)) {
|
if (!"All".equals(device)) {
|
||||||
|
|
||||||
for (int i = 0;
|
for (int i = 0;
|
||||||
i < rows.size();
|
i < rows.size();
|
||||||
@@ -1304,19 +1551,34 @@ implements SensorEventListener {
|
|||||||
DeviceRecord record =
|
DeviceRecord record =
|
||||||
rows.get(i);
|
rows.get(i);
|
||||||
|
|
||||||
if (!deviceFilter.equals(
|
if (!device.equals(
|
||||||
record.address
|
record.address
|
||||||
)) {
|
)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TrackerDatabase database = db;
|
||||||
|
|
||||||
List<LocationPoint> history =
|
List<LocationPoint> history =
|
||||||
db.history(
|
database == null
|
||||||
|
? null
|
||||||
|
: database.history(
|
||||||
record.address,
|
record.address,
|
||||||
historyFrom,
|
historyFrom,
|
||||||
historyTo
|
historyTo
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Dense histories are downsampled here, on the
|
||||||
|
* worker thread, so the WebView never receives
|
||||||
|
* thousands of markers.
|
||||||
|
*/
|
||||||
|
history =
|
||||||
|
TrackPointSampler.sample(
|
||||||
|
history,
|
||||||
|
sampleInterval
|
||||||
|
);
|
||||||
|
|
||||||
appendHistory(
|
appendHistory(
|
||||||
script,
|
script,
|
||||||
record,
|
record,
|
||||||
@@ -2334,6 +2596,13 @@ implements SensorEventListener {
|
|||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
|
|
||||||
|
destroyed = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
mapExecutor.shutdownNow();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (web != null) {
|
if (web != null) {
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package com.wytehat.btlogger;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downsamples dense GPS history before it is turned into map markers.
|
||||||
|
*
|
||||||
|
* Time-bucket sampling: the timeline is cut into fixed buckets and one
|
||||||
|
* representative point (the most accurate fix) survives per bucket. The very
|
||||||
|
* first and last fixes are always kept so the trail keeps its real endpoints.
|
||||||
|
*
|
||||||
|
* A hard ceiling is applied afterwards - even "All Points" cannot hand the
|
||||||
|
* WebView an unbounded number of circle markers.
|
||||||
|
*/
|
||||||
|
final class TrackPointSampler {
|
||||||
|
|
||||||
|
/** Ceiling on rendered points, applied regardless of the chosen interval. */
|
||||||
|
static final int MAX_RENDERED_POINTS = 750;
|
||||||
|
|
||||||
|
private TrackPointSampler() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param bucketMillis bucket width, or 0 for "All Points" (cap only).
|
||||||
|
*/
|
||||||
|
static List<LocationPoint> sample(List<LocationPoint> points, long bucketMillis) {
|
||||||
|
|
||||||
|
if (points == null || points.size() < 3) {
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LocationPoint> reduced =
|
||||||
|
bucketMillis > 0
|
||||||
|
? bucket(points, bucketMillis)
|
||||||
|
: points;
|
||||||
|
|
||||||
|
return cap(reduced);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<LocationPoint> bucket(List<LocationPoint> points, long bucketMillis) {
|
||||||
|
|
||||||
|
ArrayList<LocationPoint> result = new ArrayList<LocationPoint>();
|
||||||
|
|
||||||
|
LocationPoint best = null;
|
||||||
|
long currentBucket = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < points.size(); i++) {
|
||||||
|
|
||||||
|
LocationPoint point = points.get(i);
|
||||||
|
|
||||||
|
if (point == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
long bucketIndex = point.timestamp / bucketMillis;
|
||||||
|
|
||||||
|
if (best == null) {
|
||||||
|
best = point;
|
||||||
|
currentBucket = bucketIndex;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bucketIndex != currentBucket) {
|
||||||
|
result.add(best);
|
||||||
|
best = point;
|
||||||
|
currentBucket = bucketIndex;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moreAccurate(point, best)) {
|
||||||
|
best = point;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (best != null) {
|
||||||
|
result.add(best);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Always terminate on the newest fix - that is the one the user cares
|
||||||
|
* about when reading a trail.
|
||||||
|
*/
|
||||||
|
LocationPoint last = points.get(points.size() - 1);
|
||||||
|
|
||||||
|
if (last != null &&
|
||||||
|
(result.isEmpty() ||
|
||||||
|
result.get(result.size() - 1) != last)) {
|
||||||
|
|
||||||
|
result.add(last);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<LocationPoint> cap(List<LocationPoint> points) {
|
||||||
|
|
||||||
|
int size = points.size();
|
||||||
|
|
||||||
|
if (size <= MAX_RENDERED_POINTS) {
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
int stride = (size + MAX_RENDERED_POINTS - 1) / MAX_RENDERED_POINTS;
|
||||||
|
|
||||||
|
ArrayList<LocationPoint> result = new ArrayList<LocationPoint>();
|
||||||
|
|
||||||
|
for (int i = 0; i < size; i += stride) {
|
||||||
|
result.add(points.get(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationPoint last = points.get(size - 1);
|
||||||
|
|
||||||
|
if (result.isEmpty() ||
|
||||||
|
result.get(result.size() - 1) != last) {
|
||||||
|
|
||||||
|
result.add(last);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean moreAccurate(LocationPoint candidate, LocationPoint current) {
|
||||||
|
|
||||||
|
if (candidate.accuracy <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return current.accuracy <= 0 ||
|
||||||
|
candidate.accuracy < current.accuracy;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,10 +17,25 @@ public class TrackerDatabase extends SQLiteOpenHelper {
|
|||||||
super(context, DB_NAME, null, DB_VERSION);
|
super(context, DB_NAME, null, DB_VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<LocationPoint> history(String address, long historyFrom, long historyTo)
|
public synchronized List<LocationPoint> history(String address, long historyFrom, long historyTo)
|
||||||
{
|
{
|
||||||
// TODO: Implement this method
|
ArrayList<LocationPoint> result = new ArrayList<LocationPoint>();
|
||||||
return null;
|
if (address == null) return result;
|
||||||
|
Cursor cursor = getReadableDatabase().query("location_history", null,
|
||||||
|
"address=? AND logged_at>=? AND logged_at<=?",
|
||||||
|
new String[] { address, String.valueOf(historyFrom), String.valueOf(historyTo) },
|
||||||
|
null, null, "logged_at ASC");
|
||||||
|
try {
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
LocationPoint point = new LocationPoint();
|
||||||
|
point.latitude = cursor.getDouble(cursor.getColumnIndex("latitude"));
|
||||||
|
point.longitude = cursor.getDouble(cursor.getColumnIndex("longitude"));
|
||||||
|
point.accuracy = cursor.getFloat(cursor.getColumnIndex("accuracy"));
|
||||||
|
point.timestamp = cursor.getLong(cursor.getColumnIndex("logged_at"));
|
||||||
|
result.add(point);
|
||||||
|
}
|
||||||
|
} finally { cursor.close(); }
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onCreate(SQLiteDatabase db) {
|
public void onCreate(SQLiteDatabase db) {
|
||||||
|
|||||||
Reference in New Issue
Block a user