added grid

This commit is contained in:
2026-06-11 10:22:36 +03:00
parent c2f26c8ec3
commit d28391c71f
5 changed files with 254 additions and 43 deletions
@@ -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,22 +1250,63 @@ 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;
}
applyFitBounds(fitPoints, singlePoint);
};
if (isMapLayoutReady()) {
pendingFitRunnable.run();
} else {
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;
@@ -1250,19 +1324,14 @@ public class MapFragment extends Fragment {
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);
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);
};
if (mapView.getWidth() > 0 && mapView.getHeight() > 0) {
apply.run();
} else {
mapView.post(apply);
}
saveCameraState();
}
}
+12
View File
@@ -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"
+5 -1
View File
@@ -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>