Revert "feat(map): add GPS point downsampling and UI density control"

This reverts commit c15dd28f3b.
This commit is contained in:
n0tst3v3
2026-08-19 00:49:20 -06:00
parent b8401b3613
commit d5150ba126
3 changed files with 114 additions and 532 deletions
@@ -1,5 +1,3 @@
package com.wytehat.btlogger;
import android.app.Activity; import android.app.Activity;
import android.app.DatePickerDialog; import android.app.DatePickerDialog;
import android.app.TimePickerDialog; import android.app.TimePickerDialog;
@@ -16,8 +14,6 @@ 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;
@@ -36,8 +32,6 @@ 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 {
@@ -54,9 +48,8 @@ implements SensorEventListener {
private LinearLayout filterPanel; private LinearLayout filterPanel;
private Spinner categorySpinner; private Spinner categorySpinner;
private Spinner deviceSpinner; private Spinner deviceSpinner;
private Spinner resolutionSpinner;
private CheckBox showAll; private CheckBox showAll;
@@ -75,54 +68,15 @@ implements SensorEventListener {
private final ArrayList<String> deviceLabels = private final ArrayList<String> deviceLabels =
new ArrayList<String>(); new ArrayList<String>();
private final ArrayList<String> resolutionLabels = private String categoryFilter = "Trackers / Tags";
new ArrayList<String>(); private String deviceFilter = "All";
/* private boolean rebuildingCategories;
* Time-bucket widths for the trail sampler. private boolean rebuildingDevices;
* 0 = every stored point (still capped by TrackPointSampler). private boolean loadingMap;
*/
private static final long[] RESOLUTION_VALUES = {
0L,
15L * 60L * 1000L,
60L * 60L * 1000L,
120L * 60L * 1000L
};
private static final String[] RESOLUTION_LABELS = { private long historyFrom;
"All points", private long historyTo;
"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 rebuildingDevices;
private boolean rebuildingResolution = true;
private volatile boolean loadingMap;
private volatile long historyFrom;
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;
@@ -313,53 +267,14 @@ implements SensorEventListener {
deviceSpinner.setAdapter(deviceAdapter); deviceSpinner.setAdapter(deviceAdapter);
filterPanel.addView( filterPanel.addView(
deviceSpinner, deviceSpinner,
compactParams() compactParams()
); );
/* /*
* Trail resolution (downsampling). * Time.
*/ */
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.
*/
TextView timeLabel = TextView timeLabel =
smallLabel("Location history"); smallLabel("Location history");
@@ -650,51 +565,7 @@ implements SensorEventListener {
} }
); );
/* fromButton.setOnClickListener(
* 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(
new View.OnClickListener() { new View.OnClickListener() {
@Override @Override
@@ -806,11 +677,9 @@ implements SensorEventListener {
} }
); );
updateTimeButtons(); updateTimeButtons();
rebuildingResolution = false; rebuildDeviceFilter();
rebuildDeviceFilter();
/* /*
* The initial map is deliberately: * The initial map is deliberately:
@@ -1020,28 +889,12 @@ implements SensorEventListener {
); );
} }
private void rebuildDeviceFilter() { private void rebuildDeviceFilter() {
rebuildDeviceFilter( List<DeviceRecord> available =
getMapDevices( getMapDevices();
showAll.isChecked()
)
);
}
/* ArrayList<String> nextAddresses =
* 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 =
new ArrayList<String>(); new ArrayList<String>();
ArrayList<String> nextLabels = ArrayList<String> nextLabels =
@@ -1138,19 +991,10 @@ 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 =
db.all();
List<DeviceRecord> all =
database == null
? null
: database.all();
ArrayList<DeviceRecord> result = ArrayList<DeviceRecord> result =
new ArrayList<DeviceRecord>(); new ArrayList<DeviceRecord>();
@@ -1174,17 +1018,17 @@ implements SensorEventListener {
* Normal map: * Normal map:
* only explicitly map-enabled devices. * only explicitly map-enabled devices.
*/ */
if (!includeAll && if (!showAll.isChecked() &&
record.mapEnabled != 1) { record.mapEnabled != 1) {
continue; continue;
} }
/* /*
* Normal map excludes unknown entries. * Normal map excludes unknown entries.
*/ */
if (!includeAll && if (!showAll.isChecked() &&
isUnknown(record)) { isUnknown(record)) {
continue; continue;
} }
@@ -1365,175 +1209,82 @@ implements SensorEventListener {
return 0; return 0;
} }
private void reloadMap() { private void reloadMap() {
if (loadingMap) { if (loadingMap) {
return; return;
} }
loadingMap = true; loadingMap = true;
mapReady = false; mapReady = false;
/* try {
* 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( List<DeviceRecord> available =
new Runnable() { getMapDevices();
@Override refreshMapCategories(
public void run() { available
);
List<DeviceRecord> available = null; /*
String html = null; * Rebuild device list after category
* changes.
*/
rebuildDeviceFilter();
try { ArrayList<DeviceRecord> rows =
new ArrayList<DeviceRecord>();
available = for (int i = 0;
getMapDevices(includeAll); i < available.size();
i++) {
String device = deviceFilter; DeviceRecord record =
available.get(i);
/* if (record == null ||
* The selected device may have gone away since !record.hasLocation) {
* the last load; fall back to showing all. continue;
*/ }
if (!"All".equals(device) &&
!containsAddress(
available,
device
)) {
device = "All"; if (!categoryMatches(record)) {
} continue;
}
ArrayList<DeviceRecord> rows = if (!"All".equals(deviceFilter) &&
new ArrayList<DeviceRecord>(); !deviceFilter.equals(
record.address
)) {
continue;
}
for (int i = 0; rows.add(record);
i < available.size(); }
i++) {
DeviceRecord record = String html =
available.get(i); buildHtml(
rows,
focus
);
if (record == null || web.loadDataWithBaseURL(
!record.hasLocation) { "https://unpkg.com/",
continue; html,
} "text/html",
"UTF-8",
null
);
if (!categoryMatches(record)) { } finally {
continue;
}
if (!"All".equals(device) && loadingMap = false;
!device.equals( }
record.address }
)) {
continue;
}
rows.add(record); private String buildHtml(
} List<DeviceRecord> rows,
String selectedAddress) {
html =
buildHtml(
rows,
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(
"https://unpkg.com/",
content,
"text/html",
"UTF-8",
null
);
}
} finally {
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(
List<DeviceRecord> rows,
String selectedAddress,
String device) {
StringBuilder script = StringBuilder script =
new StringBuilder(); new StringBuilder();
@@ -1542,44 +1293,29 @@ 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(device)) { if (!"All".equals(deviceFilter)) {
for (int i = 0; for (int i = 0;
i < rows.size(); i < rows.size();
i++) { i++) {
DeviceRecord record = DeviceRecord record =
rows.get(i); rows.get(i);
if (!device.equals( if (!deviceFilter.equals(
record.address record.address
)) { )) {
continue; continue;
} }
TrackerDatabase database = db; List<LocationPoint> history =
db.history(
record.address,
historyFrom,
historyTo
);
List<LocationPoint> history = appendHistory(
database == null
? null
: database.history(
record.address,
historyFrom,
historyTo
);
/*
* Dense histories are downsampled here, on the
* worker thread, so the WebView never receives
* thousands of markers.
*/
history =
TrackPointSampler.sample(
history,
sampleInterval
);
appendHistory(
script, script,
record, record,
history, history,
@@ -2594,18 +2330,11 @@ implements SensorEventListener {
} }
@Override @Override
protected void onDestroy() { protected void onDestroy() {
destroyed = true; try {
try { if (web != null) {
mapExecutor.shutdownNow();
} catch (Exception ignored) {
}
try {
if (web != null) {
web.stopLoading(); web.stopLoading();
web.destroy(); web.destroy();
web = null; web = null;
@@ -1,132 +0,0 @@
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,25 +17,10 @@ public class TrackerDatabase extends SQLiteOpenHelper {
super(context, DB_NAME, null, DB_VERSION); super(context, DB_NAME, null, DB_VERSION);
} }
public synchronized List<LocationPoint> history(String address, long historyFrom, long historyTo) public List<LocationPoint> history(String address, long historyFrom, long historyTo)
{ {
ArrayList<LocationPoint> result = new ArrayList<LocationPoint>(); // TODO: Implement this method
if (address == null) return result; return null;
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) {