generated from Grigo/AndroidTemplate
update
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package com.grigowashere.loratester;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
@@ -285,6 +286,7 @@ public class TelemetryUploader implements TelnetClient.Listener {
|
||||
}
|
||||
TelemetryPayload payload = new TelemetryPayload(
|
||||
settings.getOrCreateDeviceId(),
|
||||
phoneLabel(),
|
||||
validLat(),
|
||||
validLon(),
|
||||
stats.rssi,
|
||||
@@ -297,6 +299,13 @@ public class TelemetryUploader implements TelnetClient.Listener {
|
||||
uploadExecutor.execute(() -> uploadTelemetry(payload));
|
||||
}
|
||||
|
||||
private static String phoneLabel() {
|
||||
String manufacturer = Build.MANUFACTURER != null ? Build.MANUFACTURER : "";
|
||||
String model = Build.MODEL != null ? Build.MODEL : "";
|
||||
String label = (manufacturer + " " + model).trim();
|
||||
return label.isEmpty() ? null : label;
|
||||
}
|
||||
|
||||
private void uploadTelemetry(TelemetryPayload payload) {
|
||||
if (networkMonitor.isOnline()) {
|
||||
try {
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.grigowashere.loratester.api;
|
||||
|
||||
public class DeviceInfo {
|
||||
public String device_id;
|
||||
public String label;
|
||||
public double last_seen;
|
||||
public Double lat;
|
||||
public Double lon;
|
||||
|
||||
@@ -51,6 +51,9 @@ public class ServerApi {
|
||||
public void postTelemetry(TelemetryPayload payload) throws IOException {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("device_id", payload.deviceId);
|
||||
if (payload.deviceLabel != null && !payload.deviceLabel.isBlank()) {
|
||||
body.put("device_label", payload.deviceLabel);
|
||||
}
|
||||
if (payload.lat != null) body.put("lat", payload.lat);
|
||||
if (payload.lon != null) body.put("lon", payload.lon);
|
||||
if (payload.rssi != null) body.put("rssi", payload.rssi);
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.grigowashere.loratester.api;
|
||||
|
||||
public class TelemetryPayload {
|
||||
public final String deviceId;
|
||||
public final String deviceLabel;
|
||||
public final Double lat;
|
||||
public final Double lon;
|
||||
public final Double rssi;
|
||||
@@ -22,8 +23,24 @@ public class TelemetryPayload {
|
||||
String meta,
|
||||
String role,
|
||||
Double ts
|
||||
) {
|
||||
this(deviceId, null, lat, lon, rssi, rangeM, rawFrame, meta, role, ts);
|
||||
}
|
||||
|
||||
public TelemetryPayload(
|
||||
String deviceId,
|
||||
String deviceLabel,
|
||||
Double lat,
|
||||
Double lon,
|
||||
Double rssi,
|
||||
Double rangeM,
|
||||
String rawFrame,
|
||||
String meta,
|
||||
String role,
|
||||
Double ts
|
||||
) {
|
||||
this.deviceId = deviceId;
|
||||
this.deviceLabel = deviceLabel;
|
||||
this.lat = lat;
|
||||
this.lon = lon;
|
||||
this.rssi = rssi;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.grigowashere.loratester.map;
|
||||
|
||||
import org.mapsforge.core.model.Tile;
|
||||
import org.mapsforge.map.layer.download.tilesource.OnlineTileSource;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
|
||||
/** Esri World Imagery — tile path is zoom/y/x (not OSM zoom/x/y). */
|
||||
public final class EsriWorldImagery extends OnlineTileSource {
|
||||
|
||||
public static final EsriWorldImagery INSTANCE = new EsriWorldImagery();
|
||||
|
||||
private EsriWorldImagery() {
|
||||
super(new String[]{"server.arcgisonline.com"}, 443);
|
||||
setName("Esri.WorldImagery")
|
||||
.setAlpha(false)
|
||||
.setBaseUrl("/ArcGIS/rest/services/World_Imagery/MapServer/tile/")
|
||||
.setExtension("png")
|
||||
.setParallelRequestsLimit(4)
|
||||
.setProtocol("https")
|
||||
.setTileSize(256)
|
||||
.setZoomLevelMax((byte) 18)
|
||||
.setZoomLevelMin((byte) 0);
|
||||
setUserAgent("LoraTester/1.0");
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getTileUrl(Tile tile) throws MalformedURLException {
|
||||
StringBuilder path = new StringBuilder(48);
|
||||
path.append(getBaseUrl());
|
||||
path.append(tile.zoomLevel).append('/');
|
||||
path.append(tile.tileY).append('/');
|
||||
path.append(tile.tileX).append('.').append(getExtension());
|
||||
return new URL(getProtocol(), getHostName(), 443, path.toString());
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,8 @@ import com.grigowashere.loratester.api.TrackDetail;
|
||||
import com.grigowashere.loratester.api.TrackInfo;
|
||||
import com.grigowashere.loratester.location.GeoUtils;
|
||||
import com.grigowashere.loratester.net.NetworkMonitor;
|
||||
import com.grigowashere.loratester.map.EsriWorldImagery;
|
||||
import com.grigowashere.loratester.model.RadioSnapshot;
|
||||
import com.grigowashere.loratester.telnet.StatsExtractor;
|
||||
import com.grigowashere.loratester.track.TrackRecorder;
|
||||
|
||||
@@ -51,6 +53,7 @@ import org.mapsforge.map.layer.Layer;
|
||||
import org.mapsforge.map.layer.cache.TileCache;
|
||||
import org.mapsforge.map.layer.download.TileDownloadLayer;
|
||||
import org.mapsforge.map.layer.download.tilesource.OpenStreetMapMapnik;
|
||||
import org.mapsforge.map.layer.download.tilesource.TileSource;
|
||||
import org.mapsforge.map.layer.overlay.Marker;
|
||||
import org.mapsforge.map.layer.overlay.Polyline;
|
||||
import org.mapsforge.map.model.MapViewPosition;
|
||||
@@ -122,6 +125,7 @@ public class MapFragment extends Fragment {
|
||||
private MaterialButtonToggleGroup mapCenterMode;
|
||||
private Spinner trackSpinner;
|
||||
private Spinner mapHeatmapRadius;
|
||||
private Spinner mapBasemap;
|
||||
private TextView mapHeatmapStatus;
|
||||
private View mapHeatmapLegend;
|
||||
private List<TrackInfo> savedTracks = new ArrayList<>();
|
||||
@@ -150,6 +154,8 @@ public class MapFragment extends Fragment {
|
||||
private double lastHeatmapLat = Double.NaN;
|
||||
private double lastHeatmapLon = Double.NaN;
|
||||
private boolean suppressHeatmapSpinner;
|
||||
private boolean suppressBasemapSpinner;
|
||||
private TileSource currentTileSource = OpenStreetMapMapnik.INSTANCE;
|
||||
private Runnable heatmapReloadRunnable;
|
||||
private boolean suppressCenterToggle;
|
||||
private boolean mapGestureActive;
|
||||
@@ -196,6 +202,7 @@ public class MapFragment extends Fragment {
|
||||
mapToolDrawer = view.findViewById(R.id.mapToolDrawer);
|
||||
mapCenterMode = view.findViewById(R.id.mapCenterMode);
|
||||
mapHeatmapRadius = view.findViewById(R.id.mapHeatmapRadius);
|
||||
mapBasemap = view.findViewById(R.id.mapBasemap);
|
||||
mapHeatmapStatus = view.findViewById(R.id.mapHeatmapStatus);
|
||||
mapHeatmapLegend = view.findViewById(R.id.mapHeatmapLegend);
|
||||
trackSpinner = view.findViewById(R.id.trackSpinner);
|
||||
@@ -208,6 +215,7 @@ public class MapFragment extends Fragment {
|
||||
btnFindHill.setOnClickListener(v -> toggleHill());
|
||||
}
|
||||
setupHeatmapUi();
|
||||
setupBasemapUi();
|
||||
|
||||
updateConnectionIcons(lastDevices, serverConnected);
|
||||
|
||||
@@ -347,18 +355,18 @@ public class MapFragment extends Fragment {
|
||||
mapView.getModel().frameBufferModel.getOverdrawFactor()
|
||||
);
|
||||
|
||||
OpenStreetMapMapnik tileSource = OpenStreetMapMapnik.INSTANCE;
|
||||
tileSource.setUserAgent("LoraTester/1.0");
|
||||
OpenStreetMapMapnik.INSTANCE.setUserAgent("LoraTester/1.0");
|
||||
currentTileSource = OpenStreetMapMapnik.INSTANCE;
|
||||
|
||||
downloadLayer = new TileDownloadLayer(
|
||||
tileCache,
|
||||
mapView.getModel().mapViewPosition,
|
||||
tileSource,
|
||||
currentTileSource,
|
||||
AndroidGraphicFactory.INSTANCE
|
||||
);
|
||||
mapView.getLayerManager().getLayers().add(downloadLayer);
|
||||
mapView.setZoomLevelMin(tileSource.getZoomLevelMin());
|
||||
mapView.setZoomLevelMax(tileSource.getZoomLevelMax());
|
||||
mapView.setZoomLevelMin(currentTileSource.getZoomLevelMin());
|
||||
mapView.setZoomLevelMax(currentTileSource.getZoomLevelMax());
|
||||
downloadLayer.start();
|
||||
|
||||
MapViewPosition position = (MapViewPosition) mapView.getModel().mapViewPosition;
|
||||
@@ -707,29 +715,76 @@ public class MapFragment extends Fragment {
|
||||
}
|
||||
clearTrackLayers();
|
||||
|
||||
List<LatLong> line = new ArrayList<>();
|
||||
List<LatLong> boundsPoints = new ArrayList<>();
|
||||
boolean colorByQuality = isRxTrack(detail);
|
||||
int defaultColor = colorByQuality ? ARGB_RX : ARGB_TRACK;
|
||||
|
||||
for (TrackDetail.TrackPoint p : detail.points) {
|
||||
for (int i = 0; i < detail.points.size(); i++) {
|
||||
TrackDetail.TrackPoint p = detail.points.get(i);
|
||||
LatLong latLong = new LatLong(p.lat, p.lon);
|
||||
line.add(latLong);
|
||||
boundsPoints.add(latLong);
|
||||
Marker marker = new Marker(latLong, bitmapTrackPoint, 0, 0);
|
||||
int pointColor = trackPointColor(p.meta, colorByQuality, defaultColor);
|
||||
Marker marker = new Marker(latLong, MapsforgeBitmaps.dot(pointColor, 12), 0, 0);
|
||||
addTrackLayer(marker);
|
||||
}
|
||||
|
||||
if (line.size() >= 2) {
|
||||
Polyline polyline = new Polyline(
|
||||
MapsforgeBitmaps.linePaint(Color.GREEN, 4f),
|
||||
AndroidGraphicFactory.INSTANCE
|
||||
);
|
||||
polyline.getLatLongs().addAll(line);
|
||||
addTrackLayer(polyline);
|
||||
if (i > 0) {
|
||||
TrackDetail.TrackPoint prev = detail.points.get(i - 1);
|
||||
Double qa = rxQualityFromMeta(prev.meta);
|
||||
Double qb = rxQualityFromMeta(p.meta);
|
||||
Double q = qa != null && qb != null ? (qa + qb) / 2.0 : (qa != null ? qa : qb);
|
||||
int segColor = colorByQuality && q != null ? qualityArgb(q) : defaultColor;
|
||||
Polyline segment = new Polyline(
|
||||
MapsforgeBitmaps.linePaint(segColor, 4f),
|
||||
AndroidGraphicFactory.INSTANCE
|
||||
);
|
||||
segment.getLatLongs().add(new LatLong(prev.lat, prev.lon));
|
||||
segment.getLatLongs().add(latLong);
|
||||
addTrackLayer(segment);
|
||||
}
|
||||
}
|
||||
|
||||
fitBoundsOnce(boundsPoints, detail.points.size() == 1, true);
|
||||
}
|
||||
|
||||
private static boolean isRxTrack(TrackDetail detail) {
|
||||
if (detail.points == null) {
|
||||
return false;
|
||||
}
|
||||
for (TrackDetail.TrackPoint p : detail.points) {
|
||||
if (StatsExtractor.ROLE_RX.equals(p.role)) {
|
||||
return true;
|
||||
}
|
||||
RadioSnapshot snap = RadioSnapshot.fromMeta(p.meta, null, null);
|
||||
if (StatsExtractor.ROLE_RX.equals(snap.role)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Double rxQualityFromMeta(String meta) {
|
||||
if (meta == null || meta.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
RadioSnapshot snap = RadioSnapshot.fromMeta(meta, null, null);
|
||||
return snap.rxQualityPercent;
|
||||
}
|
||||
|
||||
private static int qualityArgb(double pct) {
|
||||
double p = Math.max(0.0, Math.min(100.0, pct));
|
||||
int r = p < 50.0 ? (int) Math.round(255.0 * (p / 50.0)) : 255;
|
||||
int g = p < 50.0 ? 255 : (int) Math.round(255.0 * (1.0 - (p - 50.0) / 50.0));
|
||||
return 0xFF000000 | (r << 16) | (g << 8);
|
||||
}
|
||||
|
||||
private static int trackPointColor(String meta, boolean colorByQuality, int fallback) {
|
||||
if (!colorByQuality) {
|
||||
return fallback;
|
||||
}
|
||||
Double q = rxQualityFromMeta(meta);
|
||||
return q != null ? qualityArgb(q) : fallback;
|
||||
}
|
||||
|
||||
private void addTrackLayer(Layer layer) {
|
||||
mapView.getLayerManager().getLayers().add(layer);
|
||||
trackLayers.add(layer);
|
||||
@@ -1015,6 +1070,68 @@ public class MapFragment extends Fragment {
|
||||
hillPathLine = null;
|
||||
}
|
||||
|
||||
private void setupBasemapUi() {
|
||||
if (mapBasemap == null) {
|
||||
return;
|
||||
}
|
||||
List<String> labels = List.of(
|
||||
getString(R.string.map_layer_scheme),
|
||||
getString(R.string.map_layer_satellite)
|
||||
);
|
||||
suppressBasemapSpinner = true;
|
||||
mapBasemap.setAdapter(new ArrayAdapter<>(
|
||||
requireContext(),
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
labels
|
||||
));
|
||||
mapBasemap.setSelection(0, false);
|
||||
suppressBasemapSpinner = false;
|
||||
mapBasemap.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View v, int pos, long id) {
|
||||
if (suppressBasemapSpinner) {
|
||||
return;
|
||||
}
|
||||
TileSource next = pos == 1 ? EsriWorldImagery.INSTANCE : OpenStreetMapMapnik.INSTANCE;
|
||||
if (next != currentTileSource) {
|
||||
switchBasemap(next);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void switchBasemap(TileSource tileSource) {
|
||||
if (!isMapReady() || tileSource == null || tileSource == currentTileSource) {
|
||||
return;
|
||||
}
|
||||
if (tileSource instanceof OpenStreetMapMapnik osm) {
|
||||
osm.setUserAgent("LoraTester/1.0");
|
||||
}
|
||||
if (downloadLayer != null) {
|
||||
downloadLayer.onPause();
|
||||
mapView.getLayerManager().getLayers().remove(downloadLayer);
|
||||
}
|
||||
currentTileSource = tileSource;
|
||||
downloadLayer = new TileDownloadLayer(
|
||||
tileCache,
|
||||
mapView.getModel().mapViewPosition,
|
||||
currentTileSource,
|
||||
AndroidGraphicFactory.INSTANCE
|
||||
);
|
||||
mapView.getLayerManager().getLayers().add(0, downloadLayer);
|
||||
mapView.setZoomLevelMin(currentTileSource.getZoomLevelMin());
|
||||
mapView.setZoomLevelMax(currentTileSource.getZoomLevelMax());
|
||||
downloadLayer.start();
|
||||
if (mapResumed) {
|
||||
downloadLayer.onResume();
|
||||
}
|
||||
requestMapInvalidate();
|
||||
}
|
||||
|
||||
private void setupHeatmapUi() {
|
||||
if (mapHeatmapRadius == null) {
|
||||
return;
|
||||
|
||||
@@ -35,4 +35,12 @@ final class MapsforgeBitmaps {
|
||||
paint.setStyle(org.mapsforge.core.graphics.Style.STROKE);
|
||||
return paint;
|
||||
}
|
||||
|
||||
static org.mapsforge.core.graphics.Paint linePaint(int argb, float strokeWidth) {
|
||||
int a = (argb >> 24) & 0xFF;
|
||||
int r = (argb >> 16) & 0xFF;
|
||||
int g = (argb >> 8) & 0xFF;
|
||||
int b = argb & 0xFF;
|
||||
return linePaint(AndroidGraphicFactory.INSTANCE.createColor(r, g, b, a), strokeWidth);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,6 +222,20 @@
|
||||
android:textSize="10sp" />
|
||||
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/map_layer"
|
||||
android:textColor="#CCCCCC"
|
||||
android:textSize="9sp" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/mapBasemap"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -81,6 +81,9 @@
|
||||
<string name="map_center_rx">RX</string>
|
||||
<string name="map_center_both">Оба</string>
|
||||
<string name="map_center_mode">Центр карты</string>
|
||||
<string name="map_layer">Слой карты</string>
|
||||
<string name="map_layer_scheme">Схема</string>
|
||||
<string name="map_layer_satellite">Спутник</string>
|
||||
<string name="map_center_unavailable">Нет координат для выбранного режима</string>
|
||||
<string name="map_tool_center">Центрировать карту</string>
|
||||
<string name="map_tool_track">Трекинг пути</string>
|
||||
|
||||
Reference in New Issue
Block a user