generated from Grigo/AndroidTemplate
added grid
This commit is contained in:
@@ -229,7 +229,7 @@ public class ServerApi {
|
||||
}
|
||||
}
|
||||
|
||||
public ElevationGridResult getElevationGrid(double lat, double lon, int radiusM)
|
||||
public ElevationGridResult getElevationGrid(double lat, double lon, int radiusM, int stepM)
|
||||
throws IOException {
|
||||
String path = "/api/elevation/grid?lat="
|
||||
+ lat
|
||||
@@ -237,7 +237,8 @@ public class ServerApi {
|
||||
+ lon
|
||||
+ "&radius_m="
|
||||
+ radiusM
|
||||
+ "&step_m=0";
|
||||
+ "&step_m="
|
||||
+ stepM;
|
||||
Request request = new Request.Builder()
|
||||
.url(baseUrl + path)
|
||||
.get()
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.grigowashere.loratester.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.LinearGradient;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Shader;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.grigowashere.loratester.R;
|
||||
|
||||
/** Color scale for elevation heatmap (relative to GPS center). */
|
||||
public class ElevationHeatmapLegendView extends View {
|
||||
|
||||
private static final double DELTA_MIN = -10.0;
|
||||
private static final double DELTA_MAX = 10.0;
|
||||
|
||||
private final Paint bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint barPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint labelPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint titlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final RectF barRect = new RectF();
|
||||
private final RectF bgRect = new RectF();
|
||||
|
||||
public ElevationHeatmapLegendView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public ElevationHeatmapLegendView(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public ElevationHeatmapLegendView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
bgPaint.setColor(0xCC0F3460);
|
||||
labelPaint.setColor(0xFFEEEEEE);
|
||||
labelPaint.setTextSize(sp(9));
|
||||
titlePaint.setColor(0xFFFFFFFF);
|
||||
titlePaint.setTextSize(sp(9));
|
||||
titlePaint.setFakeBoldText(true);
|
||||
setLayerType(LAYER_TYPE_SOFTWARE, null);
|
||||
}
|
||||
|
||||
private float sp(float value) {
|
||||
return value * getResources().getDisplayMetrics().scaledDensity;
|
||||
}
|
||||
|
||||
private float dp(float value) {
|
||||
return value * getResources().getDisplayMetrics().density;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
int w = (int) dp(118);
|
||||
int h = (int) dp(118);
|
||||
setMeasuredDimension(
|
||||
resolveSize(w, widthMeasureSpec),
|
||||
resolveSize(h, heightMeasureSpec));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
float pad = dp(6);
|
||||
bgRect.set(pad, pad, getWidth() - pad, getHeight() - pad);
|
||||
canvas.drawRoundRect(bgRect, dp(4), dp(4), bgPaint);
|
||||
|
||||
float titleY = bgRect.top + dp(12);
|
||||
canvas.drawText(getContext().getString(R.string.map_heatmap_legend_title),
|
||||
bgRect.left + dp(6), titleY, titlePaint);
|
||||
|
||||
float barLeft = bgRect.left + dp(8);
|
||||
float barTop = titleY + dp(6);
|
||||
float barBottom = bgRect.bottom - dp(8);
|
||||
float barRight = barLeft + dp(14);
|
||||
barRect.set(barLeft, barTop, barRight, barBottom);
|
||||
|
||||
int[] colors = sampleRampColors(24);
|
||||
float[] positions = new float[colors.length];
|
||||
for (int i = 0; i < colors.length; i++) {
|
||||
positions[i] = i / (float) (colors.length - 1);
|
||||
}
|
||||
barPaint.setShader(new LinearGradient(
|
||||
barRect.left, barRect.top, barRect.left, barRect.bottom,
|
||||
colors, positions, Shader.TileMode.CLAMP));
|
||||
canvas.drawRoundRect(barRect, dp(2), dp(2), barPaint);
|
||||
barPaint.setShader(null);
|
||||
|
||||
float textX = barRect.right + dp(6);
|
||||
drawLegendRow(canvas, textX, barRect.top + dp(4),
|
||||
getContext().getString(R.string.map_heatmap_legend_high),
|
||||
"+8 m");
|
||||
drawLegendRow(canvas, textX, (barRect.top + barRect.bottom) / 2f,
|
||||
getContext().getString(R.string.map_heatmap_legend_level),
|
||||
"0 m");
|
||||
drawLegendRow(canvas, textX, barRect.bottom - dp(4),
|
||||
getContext().getString(R.string.map_heatmap_legend_low),
|
||||
"-8 m");
|
||||
}
|
||||
|
||||
private void drawLegendRow(Canvas canvas, float x, float centerY, String label, String meters) {
|
||||
float lineH = labelPaint.getTextSize();
|
||||
canvas.drawText(label, x, centerY - dp(1), labelPaint);
|
||||
canvas.drawText(meters, x, centerY + lineH - dp(2), labelPaint);
|
||||
}
|
||||
|
||||
private static int[] sampleRampColors(int steps) {
|
||||
int[] colors = new int[steps];
|
||||
for (int i = 0; i < steps; i++) {
|
||||
double t = i / (double) (steps - 1);
|
||||
double delta = DELTA_MAX + t * (DELTA_MIN - DELTA_MAX);
|
||||
colors[i] = ElevationColorRamp.deltaToArgb(delta) | 0xFF000000;
|
||||
}
|
||||
return colors;
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,8 @@ public class MapFragment extends Fragment {
|
||||
|
||||
private static final class MapSessionState {
|
||||
static boolean initialFitDone;
|
||||
static LatLong savedCenter;
|
||||
static byte savedZoom = 12;
|
||||
}
|
||||
|
||||
private static final int TILE_SIZE_PX = 256;
|
||||
@@ -118,6 +120,7 @@ public class MapFragment extends Fragment {
|
||||
private Spinner trackSpinner;
|
||||
private Spinner mapHeatmapRadius;
|
||||
private TextView mapHeatmapStatus;
|
||||
private View mapHeatmapLegend;
|
||||
private List<TrackInfo> savedTracks = new ArrayList<>();
|
||||
private boolean mapResumed;
|
||||
private boolean mapInitialized;
|
||||
@@ -146,6 +149,7 @@ public class MapFragment extends Fragment {
|
||||
private Runnable heatmapReloadRunnable;
|
||||
private float touchDownX;
|
||||
private float touchDownY;
|
||||
private Runnable pendingFitRunnable;
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
@@ -183,6 +187,7 @@ public class MapFragment extends Fragment {
|
||||
btnHeatmap = view.findViewById(R.id.btnHeatmap);
|
||||
mapHeatmapRadius = view.findViewById(R.id.mapHeatmapRadius);
|
||||
mapHeatmapStatus = view.findViewById(R.id.mapHeatmapStatus);
|
||||
mapHeatmapLegend = view.findViewById(R.id.mapHeatmapLegend);
|
||||
btnCenterMe = view.findViewById(R.id.btnCenterMe);
|
||||
btnCenterTx = view.findViewById(R.id.btnCenterTx);
|
||||
btnCenterRx = view.findViewById(R.id.btnCenterRx);
|
||||
@@ -215,9 +220,6 @@ public class MapFragment extends Fragment {
|
||||
if (isAdded() && mapStatus != null) {
|
||||
requireActivity().runOnUiThread(this::updateNetworkStatusLine);
|
||||
}
|
||||
if (online && downloadLayer != null && mapResumed) {
|
||||
downloadLayer.onResume();
|
||||
}
|
||||
};
|
||||
if (networkMonitor != null) {
|
||||
networkMonitor.addListener(networkListener);
|
||||
@@ -268,6 +270,9 @@ public class MapFragment extends Fragment {
|
||||
}
|
||||
} else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
||||
v.getParent().requestDisallowInterceptTouchEvent(false);
|
||||
if (userMovedMap) {
|
||||
saveCameraState();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
@@ -296,8 +301,12 @@ public class MapFragment extends Fragment {
|
||||
downloadLayer.start();
|
||||
|
||||
MapViewPosition position = (MapViewPosition) mapView.getModel().mapViewPosition;
|
||||
position.setCenter(new LatLong(55.75, 37.62));
|
||||
position.setZoomLevel((byte) 10);
|
||||
if (MapSessionState.savedCenter != null) {
|
||||
position.setCenter(MapSessionState.savedCenter);
|
||||
position.setZoomLevel(MapSessionState.savedZoom);
|
||||
} else {
|
||||
position.setZoomLevel((byte) 12);
|
||||
}
|
||||
|
||||
bitmapTx = MapsforgeBitmaps.dot(ARGB_TX, 20);
|
||||
bitmapRx = MapsforgeBitmaps.dot(ARGB_RX, 20);
|
||||
@@ -326,6 +335,7 @@ public class MapFragment extends Fragment {
|
||||
@Override
|
||||
public void onPause() {
|
||||
mapResumed = false;
|
||||
saveCameraState();
|
||||
if (pollHelper != null) {
|
||||
pollHelper.stop();
|
||||
}
|
||||
@@ -339,6 +349,7 @@ public class MapFragment extends Fragment {
|
||||
public void onDestroyView() {
|
||||
mapResumed = false;
|
||||
mapInitialized = false;
|
||||
cancelPendingFit();
|
||||
if (pollHelper != null) {
|
||||
pollHelper.stop();
|
||||
}
|
||||
@@ -375,6 +386,7 @@ public class MapFragment extends Fragment {
|
||||
btnHeatmap = null;
|
||||
mapHeatmapRadius = null;
|
||||
mapHeatmapStatus = null;
|
||||
mapHeatmapLegend = null;
|
||||
mapHillStatus = null;
|
||||
heatmapLayer = null;
|
||||
heatmapActive = false;
|
||||
@@ -938,11 +950,18 @@ public class MapFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateHeatmapLegendVisibility() {
|
||||
if (mapHeatmapLegend != null) {
|
||||
mapHeatmapLegend.setVisibility(heatmapActive ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void toggleHeatmap() {
|
||||
heatmapActive = !heatmapActive;
|
||||
if (btnHeatmap != null) {
|
||||
btnHeatmap.setActivated(heatmapActive);
|
||||
}
|
||||
updateHeatmapLegendVisibility();
|
||||
if (!heatmapActive) {
|
||||
cancelHeatmapReload();
|
||||
removeHeatmapLayer();
|
||||
@@ -956,6 +975,7 @@ public class MapFragment extends Fragment {
|
||||
LatLong self = resolveSelfPosition();
|
||||
if (self == null) {
|
||||
heatmapActive = false;
|
||||
updateHeatmapLegendVisibility();
|
||||
if (btnHeatmap != null) {
|
||||
btnHeatmap.setActivated(false);
|
||||
}
|
||||
@@ -965,10 +985,22 @@ public class MapFragment extends Fragment {
|
||||
loadHeatmap(self.latitude, self.longitude, heatmapRadiusM);
|
||||
}
|
||||
|
||||
/** Finer step for small radius, coarser for large (meters). */
|
||||
private static int heatmapStepForRadius(int radiusM) {
|
||||
if (radiusM <= 100) {
|
||||
return 8;
|
||||
}
|
||||
if (radiusM <= 200) {
|
||||
return 12;
|
||||
}
|
||||
return 18;
|
||||
}
|
||||
|
||||
private void loadHeatmap(double lat, double lon, int radiusM) {
|
||||
if (uploader == null || !isAdded()) {
|
||||
return;
|
||||
}
|
||||
final int stepM = heatmapStepForRadius(radiusM);
|
||||
if (mapHeatmapStatus != null) {
|
||||
mapHeatmapStatus.setVisibility(View.VISIBLE);
|
||||
mapHeatmapStatus.setText(R.string.map_heatmap_loading);
|
||||
@@ -978,7 +1010,7 @@ public class MapFragment extends Fragment {
|
||||
}
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
ElevationGridResult grid = uploader.getServerApi().getElevationGrid(lat, lon, radiusM);
|
||||
ElevationGridResult grid = uploader.getServerApi().getElevationGrid(lat, lon, radiusM, stepM);
|
||||
if (!isAdded()) {
|
||||
return;
|
||||
}
|
||||
@@ -1007,6 +1039,7 @@ public class MapFragment extends Fragment {
|
||||
R.string.map_heatmap_result,
|
||||
grid.min_delta_m,
|
||||
grid.max_delta_m,
|
||||
grid.step_m > 0 ? grid.step_m : stepM,
|
||||
grid.points.size()
|
||||
));
|
||||
}
|
||||
@@ -1217,52 +1250,88 @@ public class MapFragment extends Fragment {
|
||||
MapViewPosition position = (MapViewPosition) mapView.getModel().mapViewPosition;
|
||||
position.setCenter(point);
|
||||
position.setZoomLevel(zoom);
|
||||
saveCameraState();
|
||||
mapView.invalidate();
|
||||
}
|
||||
|
||||
private void saveCameraState() {
|
||||
if (mapView == null || !mapInitialized) {
|
||||
return;
|
||||
}
|
||||
MapViewPosition position = (MapViewPosition) mapView.getModel().mapViewPosition;
|
||||
LatLong center = position.getCenter();
|
||||
if (center != null) {
|
||||
MapSessionState.savedCenter = center;
|
||||
MapSessionState.savedZoom = position.getZoomLevel();
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelPendingFit() {
|
||||
if (pendingFitRunnable != null && mapView != null) {
|
||||
mapView.removeCallbacks(pendingFitRunnable);
|
||||
}
|
||||
pendingFitRunnable = null;
|
||||
}
|
||||
|
||||
private boolean isMapLayoutReady() {
|
||||
return mapView != null && mapView.getWidth() > 0 && mapView.getHeight() > 0;
|
||||
}
|
||||
|
||||
/** Adjust camera only on first device load or when user picks a saved track. */
|
||||
private void fitBoundsOnce(List<LatLong> points, boolean singlePoint, boolean force) {
|
||||
if (!isMapReady() || points.isEmpty() || (!force && userMovedMap)) {
|
||||
return;
|
||||
}
|
||||
MapViewPosition position = (MapViewPosition) mapView.getModel().mapViewPosition;
|
||||
Runnable apply = () -> {
|
||||
if (!isMapReady()) {
|
||||
cancelPendingFit();
|
||||
final List<LatLong> fitPoints = new ArrayList<>(points);
|
||||
pendingFitRunnable = () -> {
|
||||
pendingFitRunnable = null;
|
||||
if (!isMapReady() || fitPoints.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (singlePoint) {
|
||||
position.setCenter(points.get(0));
|
||||
position.setZoomLevel((byte) 14);
|
||||
return;
|
||||
}
|
||||
double minLat = Double.MAX_VALUE;
|
||||
double maxLat = -Double.MAX_VALUE;
|
||||
double minLon = Double.MAX_VALUE;
|
||||
double maxLon = -Double.MAX_VALUE;
|
||||
for (LatLong p : points) {
|
||||
minLat = Math.min(minLat, p.latitude);
|
||||
maxLat = Math.max(maxLat, p.latitude);
|
||||
minLon = Math.min(minLon, p.longitude);
|
||||
maxLon = Math.max(maxLon, p.longitude);
|
||||
}
|
||||
double padLat = Math.max((maxLat - minLat) * 0.2, 0.003);
|
||||
double padLon = Math.max((maxLon - minLon) * 0.2, 0.003);
|
||||
BoundingBox box = new BoundingBox(
|
||||
minLat - padLat, minLon - padLon, maxLat + padLat, maxLon + padLon);
|
||||
position.setCenter(box.getCenterPoint());
|
||||
int w = Math.max(mapView.getWidth(), 1);
|
||||
int h = Math.max(mapView.getHeight(), 1);
|
||||
double latSpan = Math.max(box.maxLatitude - box.minLatitude, 0.001);
|
||||
double lonSpan = Math.max(box.maxLongitude - box.minLongitude, 0.001);
|
||||
double latZoom = Math.log(h / (double) TILE_SIZE_PX / latSpan) / Math.log(2);
|
||||
double lonZoom = Math.log(w / (double) TILE_SIZE_PX / lonSpan) / Math.log(2);
|
||||
byte zoom = (byte) Math.max(12, Math.min(16, Math.floor(Math.min(latZoom, lonZoom))));
|
||||
position.setZoomLevel(zoom);
|
||||
applyFitBounds(fitPoints, singlePoint);
|
||||
};
|
||||
if (mapView.getWidth() > 0 && mapView.getHeight() > 0) {
|
||||
apply.run();
|
||||
if (isMapLayoutReady()) {
|
||||
pendingFitRunnable.run();
|
||||
} else {
|
||||
mapView.post(apply);
|
||||
mapView.post(pendingFitRunnable);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyFitBounds(List<LatLong> points, boolean singlePoint) {
|
||||
if (!isMapReady() || !isMapLayoutReady()) {
|
||||
return;
|
||||
}
|
||||
MapViewPosition position = (MapViewPosition) mapView.getModel().mapViewPosition;
|
||||
if (singlePoint) {
|
||||
position.setCenter(points.get(0));
|
||||
position.setZoomLevel((byte) 14);
|
||||
saveCameraState();
|
||||
return;
|
||||
}
|
||||
double minLat = Double.MAX_VALUE;
|
||||
double maxLat = -Double.MAX_VALUE;
|
||||
double minLon = Double.MAX_VALUE;
|
||||
double maxLon = -Double.MAX_VALUE;
|
||||
for (LatLong p : points) {
|
||||
minLat = Math.min(minLat, p.latitude);
|
||||
maxLat = Math.max(maxLat, p.latitude);
|
||||
minLon = Math.min(minLon, p.longitude);
|
||||
maxLon = Math.max(maxLon, p.longitude);
|
||||
}
|
||||
double padLat = Math.max((maxLat - minLat) * 0.2, 0.003);
|
||||
double padLon = Math.max((maxLon - minLon) * 0.2, 0.003);
|
||||
BoundingBox box = new BoundingBox(
|
||||
minLat - padLat, minLon - padLon, maxLat + padLat, maxLon + padLon);
|
||||
position.setCenter(box.getCenterPoint());
|
||||
int w = mapView.getWidth();
|
||||
int h = mapView.getHeight();
|
||||
double latSpan = Math.max(box.maxLatitude - box.minLatitude, 0.001);
|
||||
double lonSpan = Math.max(box.maxLongitude - box.minLongitude, 0.001);
|
||||
double latZoom = Math.log(h / (double) TILE_SIZE_PX / latSpan) / Math.log(2);
|
||||
double lonZoom = Math.log(w / (double) TILE_SIZE_PX / lonSpan) / Math.log(2);
|
||||
byte zoom = (byte) Math.max(12, Math.min(16, Math.floor(Math.min(latZoom, lonZoom))));
|
||||
position.setZoomLevel(zoom);
|
||||
saveCameraState();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,18 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.grigowashere.loratester.ui.ElevationHeatmapLegendView
|
||||
android:id="@+id/mapHeatmapLegend"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:elevation="4dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/mapSidePanel"
|
||||
android:layout_width="152dp"
|
||||
|
||||
@@ -95,8 +95,12 @@
|
||||
<string name="map_heatmap_radius">Радиус рельефа</string>
|
||||
<string name="map_heatmap_radius_m">%1$d m</string>
|
||||
<string name="map_heatmap_loading">Загрузка рельефа…</string>
|
||||
<string name="map_heatmap_result">Δ %1$.0f…%2$.0f m · %3$d точек</string>
|
||||
<string name="map_heatmap_result">Δ %1$.0f…%2$.0f m · шаг %3$.0f m · %4$d точек</string>
|
||||
<string name="map_heatmap_error">Рельеф: %1$s</string>
|
||||
<string name="map_heatmap_no_gps">Нужен GPS для рельефа</string>
|
||||
<string name="map_heatmap_legend_title">Относ. высота</string>
|
||||
<string name="map_heatmap_legend_high">возвышенность</string>
|
||||
<string name="map_heatmap_legend_level">уровень</string>
|
||||
<string name="map_heatmap_legend_low">низина</string>
|
||||
<string name="chat_self_label">Вы</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user