generated from Grigo/AndroidTemplate
added linear slider
This commit is contained in:
@@ -11,7 +11,7 @@ Android-клиент и Python-сервер для мониторинга LoRa
|
||||
|
||||
1. Запустите сервер: `cd server && pip install -r requirements.txt && python flask_app.py`
|
||||
2. Соберите APK в Android Studio или `./gradlew assembleDebug`
|
||||
3. В приложении: Настройки → URL `http://<ваш-сервер>:7634`, включите telnet при наличии моста COM→telnet
|
||||
3. В приложении: Настройки → URL `https://lora.grigowashere.ru` (или свой сервер), включите telnet при наличии моста COM→telnet
|
||||
|
||||
## Тесты
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
ViewPager2 pager = findViewById(R.id.viewPager);
|
||||
TabLayout tabs = findViewById(R.id.tabLayout);
|
||||
pager.setOffscreenPageLimit(1);
|
||||
pager.setAdapter(new MainPagerAdapter(this));
|
||||
new TabLayoutMediator(tabs, pager, (tab, position) -> {
|
||||
int titleRes = switch (position) {
|
||||
|
||||
@@ -14,7 +14,8 @@ public class SettingsRepository {
|
||||
private static final String KEY_TELNET_ENABLED = "telnet_enabled";
|
||||
private static final String KEY_DEVICE_ID = "device_id";
|
||||
|
||||
public static final String DEFAULT_SERVER = "http://grigowashere.ru:7634";
|
||||
public static final String DEFAULT_SERVER = "https://lora.grigowashere.ru";
|
||||
private static final String LEGACY_SERVER_HTTP = "http://grigowashere.ru:7634";
|
||||
public static final String DEFAULT_TELNET_HOST = "127.0.0.1";
|
||||
public static final int DEFAULT_TELNET_PORT = 2727;
|
||||
public static final String DEFAULT_RSSI_REGEX = "(?:RSSI|Power)[:\\s]*(-?\\d+(?:\\.\\d+)?)";
|
||||
@@ -25,6 +26,28 @@ public class SettingsRepository {
|
||||
public SettingsRepository(Context context) {
|
||||
prefs = context.getApplicationContext()
|
||||
.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
|
||||
migrateLegacyServerUrl();
|
||||
}
|
||||
|
||||
private void migrateLegacyServerUrl() {
|
||||
String current = prefs.getString(KEY_SERVER_URL, null);
|
||||
if (current == null || !isLegacyServerUrl(current)) {
|
||||
return;
|
||||
}
|
||||
prefs.edit().putString(KEY_SERVER_URL, DEFAULT_SERVER).apply();
|
||||
}
|
||||
|
||||
static boolean isLegacyServerUrl(String url) {
|
||||
if (url == null) {
|
||||
return false;
|
||||
}
|
||||
String u = url.trim().toLowerCase();
|
||||
while (u.endsWith("/")) {
|
||||
u = u.substring(0, u.length() - 1);
|
||||
}
|
||||
return u.equals(LEGACY_SERVER_HTTP)
|
||||
|| u.equals("http://grigowashere.ru")
|
||||
|| u.equals("https://grigowashere.ru:7634");
|
||||
}
|
||||
|
||||
public String getServerUrl() {
|
||||
|
||||
@@ -7,9 +7,10 @@ import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
@@ -20,6 +21,8 @@ import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.material.button.MaterialButtonToggleGroup;
|
||||
|
||||
import com.grigowashere.loratester.CommandPoller;
|
||||
import com.grigowashere.loratester.LoraApp;
|
||||
import com.grigowashere.loratester.PeerDevices;
|
||||
@@ -87,7 +90,7 @@ public class MapFragment extends Fragment {
|
||||
private static final long LORA_STATS_FRESH_MS = 120_000;
|
||||
private static final int HEATMAP_GPS_FOLLOW_M = 50;
|
||||
private static final long HEATMAP_GPS_DEBOUNCE_MS = 2000L;
|
||||
private static final int[] HEATMAP_RADIUS_OPTIONS = {100, 200, 500};
|
||||
private static final int[] HEATMAP_RADIUS_OPTIONS = {50, 100, 200, 500};
|
||||
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
private final DateFormat timeFormat =
|
||||
@@ -109,14 +112,14 @@ public class MapFragment extends Fragment {
|
||||
private TextView trackStatus;
|
||||
private ImageView iconServer;
|
||||
private ImageView iconLora;
|
||||
private Button btnTrack;
|
||||
private Button btnPairedTrack;
|
||||
private Button btnFindHill;
|
||||
private Button btnHeatmap;
|
||||
private Button btnCenterMe;
|
||||
private Button btnCenterTx;
|
||||
private Button btnCenterRx;
|
||||
private Button btnCenterBoth;
|
||||
private ImageButton btnTrack;
|
||||
private ImageButton btnPairedTrack;
|
||||
private ImageButton btnFindHill;
|
||||
private ImageButton btnHeatmap;
|
||||
private ImageButton btnToolCenter;
|
||||
private ImageButton btnToolMore;
|
||||
private View mapToolDrawer;
|
||||
private MaterialButtonToggleGroup mapCenterMode;
|
||||
private Spinner trackSpinner;
|
||||
private Spinner mapHeatmapRadius;
|
||||
private TextView mapHeatmapStatus;
|
||||
@@ -142,11 +145,13 @@ public class MapFragment extends Fragment {
|
||||
private Polyline hillPathLine;
|
||||
private ElevationHeatmapLayer heatmapLayer;
|
||||
private boolean heatmapActive;
|
||||
private int heatmapRadiusM = 200;
|
||||
private int heatmapRadiusM = 100;
|
||||
private double lastHeatmapLat = Double.NaN;
|
||||
private double lastHeatmapLon = Double.NaN;
|
||||
private boolean suppressHeatmapSpinner;
|
||||
private Runnable heatmapReloadRunnable;
|
||||
private boolean suppressCenterToggle;
|
||||
private boolean mapGestureActive;
|
||||
private float touchDownX;
|
||||
private float touchDownY;
|
||||
private Runnable pendingFitRunnable;
|
||||
@@ -185,28 +190,19 @@ public class MapFragment extends Fragment {
|
||||
btnPairedTrack = view.findViewById(R.id.btnPairedTrack);
|
||||
btnFindHill = view.findViewById(R.id.btnFindHill);
|
||||
btnHeatmap = view.findViewById(R.id.btnHeatmap);
|
||||
btnToolCenter = view.findViewById(R.id.btnToolCenter);
|
||||
btnToolMore = view.findViewById(R.id.btnToolMore);
|
||||
mapToolDrawer = view.findViewById(R.id.mapToolDrawer);
|
||||
mapCenterMode = view.findViewById(R.id.mapCenterMode);
|
||||
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);
|
||||
btnCenterBoth = view.findViewById(R.id.btnCenterBoth);
|
||||
trackSpinner = view.findViewById(R.id.trackSpinner);
|
||||
pollHelper = new FragmentPollHelper(this, this::refreshDevices);
|
||||
|
||||
if (btnCenterMe != null) {
|
||||
btnCenterMe.setOnClickListener(v -> centerOnSelf());
|
||||
}
|
||||
if (btnCenterTx != null) {
|
||||
btnCenterTx.setOnClickListener(v -> centerOnRole(StatsExtractor.ROLE_TX));
|
||||
}
|
||||
if (btnCenterRx != null) {
|
||||
btnCenterRx.setOnClickListener(v -> centerOnRole(StatsExtractor.ROLE_RX));
|
||||
}
|
||||
if (btnCenterBoth != null) {
|
||||
btnCenterBoth.setOnClickListener(v -> centerOnBoth());
|
||||
}
|
||||
setupToolRail();
|
||||
setupCenterModeToggle();
|
||||
|
||||
if (btnFindHill != null) {
|
||||
btnFindHill.setOnClickListener(v -> findNearestHill());
|
||||
}
|
||||
@@ -234,6 +230,67 @@ public class MapFragment extends Fragment {
|
||||
setupTrackSpinnerListener();
|
||||
}
|
||||
|
||||
private void setupToolRail() {
|
||||
if (btnToolCenter != null) {
|
||||
btnToolCenter.setOnClickListener(v -> {
|
||||
setMapDrawerOpen(true);
|
||||
if (mapCenterMode != null) {
|
||||
mapCenterMode.requestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (btnToolMore != null) {
|
||||
btnToolMore.setOnClickListener(v -> setMapDrawerOpen(
|
||||
mapToolDrawer == null || mapToolDrawer.getVisibility() != View.VISIBLE));
|
||||
}
|
||||
}
|
||||
|
||||
private void setMapDrawerOpen(boolean open) {
|
||||
if (mapToolDrawer != null) {
|
||||
mapToolDrawer.setVisibility(open ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
if (btnToolMore != null) {
|
||||
btnToolMore.setActivated(open);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupCenterModeToggle() {
|
||||
if (mapCenterMode == null) {
|
||||
return;
|
||||
}
|
||||
mapCenterMode.addOnButtonCheckedListener((group, checkedId, isChecked) -> {
|
||||
if (suppressCenterToggle || !isChecked) {
|
||||
return;
|
||||
}
|
||||
boolean ok;
|
||||
if (checkedId == R.id.centerMe) {
|
||||
ok = centerOnSelf();
|
||||
} else if (checkedId == R.id.centerTx) {
|
||||
ok = centerOnRole(StatsExtractor.ROLE_TX);
|
||||
} else if (checkedId == R.id.centerRx) {
|
||||
ok = centerOnRole(StatsExtractor.ROLE_RX);
|
||||
} else if (checkedId == R.id.centerBoth) {
|
||||
ok = centerOnBoth();
|
||||
} else {
|
||||
ok = true;
|
||||
}
|
||||
if (!ok) {
|
||||
Toast.makeText(requireContext(), R.string.map_center_unavailable, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
clearCenterModeSelection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void clearCenterModeSelection() {
|
||||
if (mapCenterMode == null) {
|
||||
return;
|
||||
}
|
||||
suppressCenterToggle = true;
|
||||
mapCenterMode.clearChecked();
|
||||
suppressCenterToggle = false;
|
||||
}
|
||||
|
||||
private void setupTrackSpinnerListener() {
|
||||
trackSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
@@ -252,12 +309,13 @@ public class MapFragment extends Fragment {
|
||||
|
||||
private void setupMapView() {
|
||||
mapView.setClickable(true);
|
||||
mapView.getMapScaleBar().setVisible(true);
|
||||
mapView.getMapScaleBar().setVisible(false);
|
||||
mapView.setBuiltInZoomControls(false);
|
||||
|
||||
mapView.setOnTouchListener((v, event) -> {
|
||||
int action = event.getActionMasked();
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
mapGestureActive = true;
|
||||
touchDownX = event.getX();
|
||||
touchDownY = event.getY();
|
||||
v.getParent().requestDisallowInterceptTouchEvent(true);
|
||||
@@ -270,9 +328,11 @@ public class MapFragment extends Fragment {
|
||||
}
|
||||
} else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
||||
v.getParent().requestDisallowInterceptTouchEvent(false);
|
||||
mapGestureActive = false;
|
||||
if (userMovedMap) {
|
||||
saveCameraState();
|
||||
}
|
||||
requestMapInvalidate();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
@@ -282,7 +342,7 @@ public class MapFragment extends Fragment {
|
||||
requireContext(),
|
||||
"loratester-tiles",
|
||||
TILE_SIZE_PX,
|
||||
1f,
|
||||
2.5f,
|
||||
mapView.getModel().frameBufferModel.getOverdrawFactor()
|
||||
);
|
||||
|
||||
@@ -384,6 +444,10 @@ public class MapFragment extends Fragment {
|
||||
iconLora = null;
|
||||
btnFindHill = null;
|
||||
btnHeatmap = null;
|
||||
btnToolCenter = null;
|
||||
btnToolMore = null;
|
||||
mapToolDrawer = null;
|
||||
mapCenterMode = null;
|
||||
mapHeatmapRadius = null;
|
||||
mapHeatmapStatus = null;
|
||||
mapHeatmapLegend = null;
|
||||
@@ -393,11 +457,8 @@ public class MapFragment extends Fragment {
|
||||
mapStatus = null;
|
||||
mapDistance = null;
|
||||
trackStatus = null;
|
||||
btnCenterMe = null;
|
||||
btnCenterTx = null;
|
||||
btnCenterRx = null;
|
||||
btnCenterBoth = null;
|
||||
btnTrack = null;
|
||||
btnPairedTrack = null;
|
||||
trackSpinner = null;
|
||||
pollHelper = null;
|
||||
super.onDestroyView();
|
||||
@@ -434,7 +495,9 @@ public class MapFragment extends Fragment {
|
||||
if (!isAdded() || btnTrack == null) {
|
||||
return;
|
||||
}
|
||||
btnTrack.setText(recording ? R.string.track_stop : R.string.track_start);
|
||||
btnTrack.setActivated(recording);
|
||||
btnTrack.setContentDescription(getString(
|
||||
recording ? R.string.track_stop : R.string.track_start));
|
||||
if (trackStatus != null) {
|
||||
trackStatus.setText(getString(R.string.track_status, pointCount));
|
||||
}
|
||||
@@ -494,9 +557,27 @@ public class MapFragment extends Fragment {
|
||||
} else {
|
||||
liveTrackMarker.setLatLong(pos);
|
||||
}
|
||||
requestMapInvalidate();
|
||||
}
|
||||
|
||||
private void requestMapInvalidate() {
|
||||
if (!isMapReady() || mapGestureActive) {
|
||||
return;
|
||||
}
|
||||
mapView.invalidate();
|
||||
}
|
||||
|
||||
private void requestMapInvalidateAfterLayout() {
|
||||
if (!isMapReady()) {
|
||||
return;
|
||||
}
|
||||
mapView.post(() -> {
|
||||
if (!mapGestureActive) {
|
||||
mapView.invalidate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void clearLiveTrackLayers() {
|
||||
liveTrackPoints.clear();
|
||||
if (mapView != null) {
|
||||
@@ -889,7 +970,7 @@ public class MapFragment extends Fragment {
|
||||
bounds.add(new LatLong(fromLat, fromLon));
|
||||
bounds.add(hillPos);
|
||||
fitBoundsOnce(bounds, false, true);
|
||||
mapView.invalidate();
|
||||
requestMapInvalidateAfterLayout();
|
||||
}
|
||||
|
||||
private void clearHillLayers() {
|
||||
@@ -987,13 +1068,16 @@ public class MapFragment extends Fragment {
|
||||
|
||||
/** Finer step for small radius, coarser for large (meters). */
|
||||
private static int heatmapStepForRadius(int radiusM) {
|
||||
if (radiusM <= 50) {
|
||||
return 1;
|
||||
}
|
||||
if (radiusM <= 100) {
|
||||
return 8;
|
||||
return 1;
|
||||
}
|
||||
if (radiusM <= 200) {
|
||||
return 12;
|
||||
return 3;
|
||||
}
|
||||
return 18;
|
||||
return 6;
|
||||
}
|
||||
|
||||
private void loadHeatmap(double lat, double lon, int radiusM) {
|
||||
@@ -1044,7 +1128,7 @@ public class MapFragment extends Fragment {
|
||||
));
|
||||
}
|
||||
if (mapView != null) {
|
||||
mapView.invalidate();
|
||||
requestMapInvalidate();
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
@@ -1208,39 +1292,43 @@ public class MapFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
private void centerOnSelf() {
|
||||
private boolean centerOnSelf() {
|
||||
if (uploader == null) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
String myId = uploader.getDeviceId();
|
||||
for (DeviceInfo d : lastDevices) {
|
||||
if (myId.equals(d.device_id) && GeoUtils.isValidCoordinate(d.lat, d.lon)) {
|
||||
centerOnPoint(new LatLong(d.lat, d.lon), (byte) 14);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void centerOnRole(String role) {
|
||||
private boolean centerOnRole(String role) {
|
||||
for (DeviceInfo d : lastDevices) {
|
||||
if (role.equals(d.role) && GeoUtils.isValidCoordinate(d.lat, d.lon)) {
|
||||
centerOnPoint(new LatLong(d.lat, d.lon), (byte) 14);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void centerOnBoth() {
|
||||
private boolean centerOnBoth() {
|
||||
List<LatLong> points = new ArrayList<>();
|
||||
for (DeviceInfo d : lastDevices) {
|
||||
if (GeoUtils.isValidCoordinate(d.lat, d.lon)) {
|
||||
points.add(new LatLong(d.lat, d.lon));
|
||||
}
|
||||
}
|
||||
if (!points.isEmpty()) {
|
||||
userMovedMap = false;
|
||||
fitBoundsOnce(points, points.size() == 1, true);
|
||||
if (points.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
userMovedMap = false;
|
||||
fitBoundsOnce(points, points.size() == 1, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void centerOnPoint(LatLong point, byte zoom) {
|
||||
@@ -1251,7 +1339,7 @@ public class MapFragment extends Fragment {
|
||||
position.setCenter(point);
|
||||
position.setZoomLevel(zoom);
|
||||
saveCameraState();
|
||||
mapView.invalidate();
|
||||
requestMapInvalidateAfterLayout();
|
||||
}
|
||||
|
||||
private void saveCameraState() {
|
||||
@@ -1307,6 +1395,7 @@ public class MapFragment extends Fragment {
|
||||
position.setCenter(points.get(0));
|
||||
position.setZoomLevel((byte) 14);
|
||||
saveCameraState();
|
||||
requestMapInvalidateAfterLayout();
|
||||
return;
|
||||
}
|
||||
double minLat = Double.MAX_VALUE;
|
||||
@@ -1333,5 +1422,6 @@ public class MapFragment extends Fragment {
|
||||
byte zoom = (byte) Math.max(12, Math.min(16, Math.floor(Math.min(latZoom, lonZoom))));
|
||||
position.setZoomLevel(zoom);
|
||||
saveCameraState();
|
||||
requestMapInvalidateAfterLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="#00FF88" android:state_activated="true" />
|
||||
<item android:color="#FFFFFF" />
|
||||
</selector>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#CC0F3460" />
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M3,17h2v-3H3v3zM3,12h2V9H3v3zM7,17h2v-5H7v5zM7,7h2V5H7v2zM11,17h2V7h-2v10zM15,17h2v-3h-2v3zM15,12h2V9h-2v3zM19,17h2v-7h-2v7zM19,8h2V5h-2v3z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,14c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,20c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5V19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45V19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z" />
|
||||
</vector>
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
@@ -8,175 +9,232 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.grigowashere.loratester.ui.ElevationHeatmapLegendView
|
||||
android:id="@+id/mapHeatmapLegend"
|
||||
<LinearLayout
|
||||
android:id="@+id/mapStatusChip"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_gravity="top|start"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:elevation="4dp"
|
||||
android:visibility="gone" />
|
||||
android:layout_marginEnd="56dp"
|
||||
android:background="@drawable/bg_map_panel"
|
||||
android:elevation="6dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="6dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iconServer"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:contentDescription="@string/status_server"
|
||||
android:src="@drawable/ic_link_server" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="3dp"
|
||||
android:text="@string/status_server_short"
|
||||
android:textColor="#AAAAAA"
|
||||
android:textSize="9sp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iconLora"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:contentDescription="@string/status_lora"
|
||||
android:src="@drawable/ic_link_lora" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="3dp"
|
||||
android:text="@string/status_lora_short"
|
||||
android:textColor="#AAAAAA"
|
||||
android:textSize="9sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mapStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:maxLines="2"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="10sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mapDistance"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textColor="#00FF88"
|
||||
android:textSize="9sp"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/mapToolRail"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:background="@drawable/bg_map_panel"
|
||||
android:elevation="6dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnToolCenter"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:contentDescription="@string/map_tool_center"
|
||||
android:src="@drawable/ic_center"
|
||||
app:tint="@color/map_tool_icon_tint" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnFindHill"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:contentDescription="@string/map_find_hill"
|
||||
android:src="@drawable/ic_hill"
|
||||
app:tint="@color/map_tool_icon_tint" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnHeatmap"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:contentDescription="@string/map_heatmap"
|
||||
android:src="@drawable/ic_heatmap"
|
||||
app:tint="@color/map_tool_icon_tint" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnTrack"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:contentDescription="@string/map_tool_track"
|
||||
android:src="@drawable/ic_track"
|
||||
app:tint="@color/map_tool_icon_tint" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnPairedTrack"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:contentDescription="@string/map_tool_paired"
|
||||
android:src="@drawable/ic_paired_track"
|
||||
app:tint="@color/map_tool_icon_tint" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnToolMore"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:contentDescription="@string/map_tool_more"
|
||||
android:src="@drawable/ic_more_vert"
|
||||
app:tint="@color/map_tool_icon_tint" />
|
||||
</LinearLayout>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/mapSidePanel"
|
||||
android:layout_width="152dp"
|
||||
android:id="@+id/mapToolDrawer"
|
||||
android:layout_width="148dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|top"
|
||||
android:layout_margin="6dp"
|
||||
android:background="#CC0F3460"
|
||||
android:elevation="4dp"
|
||||
android:fillViewport="false"
|
||||
android:scrollbars="none">
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="56dp"
|
||||
android:background="@drawable/bg_map_panel"
|
||||
android:elevation="6dp"
|
||||
android:scrollbars="none"
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="6dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iconServer"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:contentDescription="@string/status_server"
|
||||
android:src="@drawable/ic_link_server" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="@string/status_server_short"
|
||||
android:textColor="#AAAAAA"
|
||||
android:textSize="9sp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iconLora"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:contentDescription="@string/status_lora"
|
||||
android:src="@drawable/ic_link_lora" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="@string/status_lora_short"
|
||||
android:textColor="#AAAAAA"
|
||||
android:textSize="9sp" />
|
||||
</LinearLayout>
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mapStatus"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="10sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mapDistance"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@string/map_center_mode"
|
||||
android:textColor="#00FF88"
|
||||
android:textSize="9sp"
|
||||
android:visibility="gone" />
|
||||
android:textSize="10sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
<com.google.android.material.button.MaterialButtonToggleGroup
|
||||
android:id="@+id/mapCenterMode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="vertical"
|
||||
app:selectionRequired="false"
|
||||
app:singleSelection="true">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnCenterMe"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/centerMe"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:minHeight="32dp"
|
||||
android:text="@string/map_center_me"
|
||||
android:textSize="10sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnCenterTx"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/centerTx"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_weight="1"
|
||||
android:minHeight="32dp"
|
||||
android:text="@string/map_center_tx"
|
||||
android:textSize="10sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnCenterRx"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/centerRx"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_weight="1"
|
||||
android:minHeight="32dp"
|
||||
android:text="@string/map_center_rx"
|
||||
android:textSize="10sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnCenterBoth"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/centerBoth"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_weight="1"
|
||||
android:minHeight="32dp"
|
||||
android:text="@string/map_center_both"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnFindHill"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:drawableStart="@drawable/ic_hill"
|
||||
android:drawablePadding="6dp"
|
||||
android:minHeight="34dp"
|
||||
android:text="@string/map_find_hill"
|
||||
android:textSize="10sp" />
|
||||
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mapHillStatus"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textColor="#FFC107"
|
||||
android:textSize="9sp"
|
||||
android:visibility="gone" />
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/map_heatmap_radius"
|
||||
android:textColor="#CCCCCC"
|
||||
android:textSize="9sp" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/mapHeatmapRadius"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnHeatmap"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:minHeight="34dp"
|
||||
android:text="@string/map_heatmap"
|
||||
android:textSize="10sp" />
|
||||
android:layout_marginTop="2dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mapHeatmapStatus"
|
||||
@@ -187,33 +245,29 @@
|
||||
android:textSize="9sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mapHillStatus"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="#FFC107"
|
||||
android:textSize="9sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mapLegend"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:text="@string/map_legend"
|
||||
android:textColor="#CCCCCC"
|
||||
android:textSize="9sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnTrack"
|
||||
<Spinner
|
||||
android:id="@+id/trackSpinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:minHeight="36dp"
|
||||
android:text="@string/track_start"
|
||||
android:textSize="11sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnPairedTrack"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:minHeight="36dp"
|
||||
android:text="@string/track_paired_start"
|
||||
android:textSize="11sp" />
|
||||
android:layout_marginTop="6dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/trackStatus"
|
||||
@@ -222,13 +276,18 @@
|
||||
android:layout_marginTop="2dp"
|
||||
android:textColor="#CCCCCC"
|
||||
android:textSize="9sp" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/trackSpinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<com.grigowashere.loratester.ui.ElevationHeatmapLegendView
|
||||
android:id="@+id/mapHeatmapLegend"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|start"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:elevation="4dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
@@ -80,6 +80,12 @@
|
||||
<string name="map_center_tx">TX</string>
|
||||
<string name="map_center_rx">RX</string>
|
||||
<string name="map_center_both">Оба</string>
|
||||
<string name="map_center_mode">Центр карты</string>
|
||||
<string name="map_center_unavailable">Нет координат для выбранного режима</string>
|
||||
<string name="map_tool_center">Центрировать карту</string>
|
||||
<string name="map_tool_track">Трекинг пути</string>
|
||||
<string name="map_tool_paired">Синхр. трек TX/RX</string>
|
||||
<string name="map_tool_more">Дополнительно</string>
|
||||
<string name="map_gps_distance">GPS между устройствами: %1$s m</string>
|
||||
<string name="status_server">Связь с сервером</string>
|
||||
<string name="status_server_short">сервер</string>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.grigowashere.loratester;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class SettingsRepositoryTest {
|
||||
|
||||
@Test
|
||||
public void detectsLegacyServerUrls() {
|
||||
assertTrue(SettingsRepository.isLegacyServerUrl("http://grigowashere.ru:7634"));
|
||||
assertTrue(SettingsRepository.isLegacyServerUrl("http://grigowashere.ru:7634/"));
|
||||
assertTrue(SettingsRepository.isLegacyServerUrl("http://grigowashere.ru"));
|
||||
assertTrue(SettingsRepository.isLegacyServerUrl("https://grigowashere.ru:7634"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ignoresCurrentServerUrl() {
|
||||
assertFalse(SettingsRepository.isLegacyServerUrl(SettingsRepository.DEFAULT_SERVER));
|
||||
assertFalse(SettingsRepository.isLegacyServerUrl("https://example.com"));
|
||||
assertFalse(SettingsRepository.isLegacyServerUrl(null));
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -50,7 +50,7 @@ LORATESTER_ELEVATION_URL=http://192.168.1.109:8085/v1/elevation
|
||||
|
||||
БД хранится в volume `loratester-data` (`/data/loratester.db` внутри контейнера).
|
||||
|
||||
## Деплой (grigowashere.ru:7634)
|
||||
## Деплой (lora.grigowashere.ru)
|
||||
|
||||
```bash
|
||||
cd /srv/storage/disk2/services/LoraTester/server
|
||||
@@ -148,6 +148,6 @@ python -m pytest tests/ -v
|
||||
|
||||
## Android
|
||||
|
||||
URL: `http://grigowashere.ru:7634`. На карте: **Начать/Остановить трекинг пути** — точки с GPS, статистикой приёма и высотой (локальный Open-Meteo на сервере). Вкладка **Статистика** — история с сервера.
|
||||
URL: `https://lora.grigowashere.ru`. На карте: **Начать/Остановить трекинг пути** — точки с GPS, статистикой приёма и высотой (локальный Open-Meteo на сервере). Вкладка **Статистика** — история с сервера.
|
||||
|
||||
Telnet: `127.0.0.1:2727` — мост COM→telnet на устройстве.
|
||||
|
||||
Binary file not shown.
@@ -371,6 +371,7 @@ def _offset_m(lat: float, lon: float, north_m: float, east_m: float) -> tuple[fl
|
||||
|
||||
|
||||
_MAX_GRID_POINTS = 2500
|
||||
_MAX_GRID_POINTS_FINE = 12000
|
||||
|
||||
|
||||
def _auto_step_m(radius_m: float) -> float:
|
||||
@@ -401,11 +402,15 @@ def _sample_circular_grid(
|
||||
return cells
|
||||
|
||||
|
||||
def _resolve_grid_step(lat: float, lon: float, radius_m: float, step_m: float) -> float:
|
||||
def _resolve_grid_step(
|
||||
lat: float, lon: float, radius_m: float, step_m: float
|
||||
) -> float:
|
||||
if step_m <= 0:
|
||||
step_m = _auto_step_m(radius_m)
|
||||
step_m = max(5.0, min(float(step_m), 100.0))
|
||||
while len(_sample_circular_grid(lat, lon, radius_m, step_m)) > _MAX_GRID_POINTS:
|
||||
min_step = 1.0 if radius_m <= 100.0 else 5.0
|
||||
step_m = max(min_step, min(float(step_m), 100.0))
|
||||
max_points = _MAX_GRID_POINTS_FINE if radius_m <= 100.0 and step_m <= 1.0 else _MAX_GRID_POINTS
|
||||
while len(_sample_circular_grid(lat, lon, radius_m, step_m)) > max_points:
|
||||
step_m = math.ceil(step_m * 1.25)
|
||||
if step_m >= radius_m:
|
||||
break
|
||||
@@ -427,7 +432,7 @@ def build_elevation_grid(
|
||||
"elevation_url": ELEVATION_API_URL,
|
||||
}
|
||||
|
||||
radius_m = max(100.0, min(float(radius_m), 500.0))
|
||||
radius_m = max(50.0, min(float(radius_m), 500.0))
|
||||
step_m = _resolve_grid_step(lat, lon, radius_m, step_m)
|
||||
|
||||
center_elev = fetch_elevation_m(lat, lon)
|
||||
|
||||
@@ -349,8 +349,8 @@ def elevation_nearest_hill(
|
||||
def elevation_grid(
|
||||
lat: float = Query(..., ge=-90.0, le=90.0),
|
||||
lon: float = Query(..., ge=-180.0, le=180.0),
|
||||
radius_m: float = Query(200.0, ge=100.0, le=500.0),
|
||||
step_m: float = Query(0.0, ge=0.0, le=100.0),
|
||||
radius_m: float = Query(200.0, ge=50.0, le=500.0),
|
||||
step_m: float = Query(0.0, ge=1.0, le=100.0),
|
||||
):
|
||||
from core.elevation import build_elevation_grid
|
||||
|
||||
|
||||
Binary file not shown.
@@ -141,6 +141,22 @@ def test_build_elevation_grid_delta(monkeypatch):
|
||||
assert all("delta_m" in p for p in result["points"])
|
||||
|
||||
|
||||
def test_build_elevation_grid_fine_step_small_radius(monkeypatch):
|
||||
monkeypatch.setattr(elev, "_probe_checked_at", 0.0)
|
||||
monkeypatch.setattr(elev, "probe_elevation_api", lambda force=False: {"ok": True, "error": None})
|
||||
monkeypatch.setattr(elev, "fetch_elevation_m", lambda lat, lon: 120.0)
|
||||
monkeypatch.setattr(
|
||||
elev,
|
||||
"fetch_elevations_batch",
|
||||
lambda lats, lons: [120.0 + i * 0.1 for i in range(len(lats))],
|
||||
)
|
||||
|
||||
result = elev.build_elevation_grid(55.75, 37.62, radius_m=50, step_m=1)
|
||||
assert result["ok"] is True
|
||||
assert result["step_m"] == 1
|
||||
assert len(result["points"]) > 1000
|
||||
|
||||
|
||||
def test_build_elevation_grid_limits_points(monkeypatch):
|
||||
monkeypatch.setattr(elev, "_probe_checked_at", 0.0)
|
||||
monkeypatch.setattr(elev, "probe_elevation_api", lambda force=False: {"ok": True, "error": None})
|
||||
|
||||
Reference in New Issue
Block a user