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`
|
1. Запустите сервер: `cd server && pip install -r requirements.txt && python flask_app.py`
|
||||||
2. Соберите APK в Android Studio или `./gradlew assembleDebug`
|
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);
|
ViewPager2 pager = findViewById(R.id.viewPager);
|
||||||
TabLayout tabs = findViewById(R.id.tabLayout);
|
TabLayout tabs = findViewById(R.id.tabLayout);
|
||||||
|
pager.setOffscreenPageLimit(1);
|
||||||
pager.setAdapter(new MainPagerAdapter(this));
|
pager.setAdapter(new MainPagerAdapter(this));
|
||||||
new TabLayoutMediator(tabs, pager, (tab, position) -> {
|
new TabLayoutMediator(tabs, pager, (tab, position) -> {
|
||||||
int titleRes = switch (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_TELNET_ENABLED = "telnet_enabled";
|
||||||
private static final String KEY_DEVICE_ID = "device_id";
|
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 String DEFAULT_TELNET_HOST = "127.0.0.1";
|
||||||
public static final int DEFAULT_TELNET_PORT = 2727;
|
public static final int DEFAULT_TELNET_PORT = 2727;
|
||||||
public static final String DEFAULT_RSSI_REGEX = "(?:RSSI|Power)[:\\s]*(-?\\d+(?:\\.\\d+)?)";
|
public static final String DEFAULT_RSSI_REGEX = "(?:RSSI|Power)[:\\s]*(-?\\d+(?:\\.\\d+)?)";
|
||||||
@@ -25,6 +26,28 @@ public class SettingsRepository {
|
|||||||
public SettingsRepository(Context context) {
|
public SettingsRepository(Context context) {
|
||||||
prefs = context.getApplicationContext()
|
prefs = context.getApplicationContext()
|
||||||
.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
|
.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() {
|
public String getServerUrl() {
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import android.view.LayoutInflater;
|
|||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.view.View;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
import android.widget.Button;
|
import android.widget.ImageButton;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.Spinner;
|
import android.widget.Spinner;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
@@ -20,6 +21,8 @@ import androidx.annotation.Nullable;
|
|||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
|
import com.google.android.material.button.MaterialButtonToggleGroup;
|
||||||
|
|
||||||
import com.grigowashere.loratester.CommandPoller;
|
import com.grigowashere.loratester.CommandPoller;
|
||||||
import com.grigowashere.loratester.LoraApp;
|
import com.grigowashere.loratester.LoraApp;
|
||||||
import com.grigowashere.loratester.PeerDevices;
|
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 long LORA_STATS_FRESH_MS = 120_000;
|
||||||
private static final int HEATMAP_GPS_FOLLOW_M = 50;
|
private static final int HEATMAP_GPS_FOLLOW_M = 50;
|
||||||
private static final long HEATMAP_GPS_DEBOUNCE_MS = 2000L;
|
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 ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
private final DateFormat timeFormat =
|
private final DateFormat timeFormat =
|
||||||
@@ -109,14 +112,14 @@ public class MapFragment extends Fragment {
|
|||||||
private TextView trackStatus;
|
private TextView trackStatus;
|
||||||
private ImageView iconServer;
|
private ImageView iconServer;
|
||||||
private ImageView iconLora;
|
private ImageView iconLora;
|
||||||
private Button btnTrack;
|
private ImageButton btnTrack;
|
||||||
private Button btnPairedTrack;
|
private ImageButton btnPairedTrack;
|
||||||
private Button btnFindHill;
|
private ImageButton btnFindHill;
|
||||||
private Button btnHeatmap;
|
private ImageButton btnHeatmap;
|
||||||
private Button btnCenterMe;
|
private ImageButton btnToolCenter;
|
||||||
private Button btnCenterTx;
|
private ImageButton btnToolMore;
|
||||||
private Button btnCenterRx;
|
private View mapToolDrawer;
|
||||||
private Button btnCenterBoth;
|
private MaterialButtonToggleGroup mapCenterMode;
|
||||||
private Spinner trackSpinner;
|
private Spinner trackSpinner;
|
||||||
private Spinner mapHeatmapRadius;
|
private Spinner mapHeatmapRadius;
|
||||||
private TextView mapHeatmapStatus;
|
private TextView mapHeatmapStatus;
|
||||||
@@ -142,11 +145,13 @@ public class MapFragment extends Fragment {
|
|||||||
private Polyline hillPathLine;
|
private Polyline hillPathLine;
|
||||||
private ElevationHeatmapLayer heatmapLayer;
|
private ElevationHeatmapLayer heatmapLayer;
|
||||||
private boolean heatmapActive;
|
private boolean heatmapActive;
|
||||||
private int heatmapRadiusM = 200;
|
private int heatmapRadiusM = 100;
|
||||||
private double lastHeatmapLat = Double.NaN;
|
private double lastHeatmapLat = Double.NaN;
|
||||||
private double lastHeatmapLon = Double.NaN;
|
private double lastHeatmapLon = Double.NaN;
|
||||||
private boolean suppressHeatmapSpinner;
|
private boolean suppressHeatmapSpinner;
|
||||||
private Runnable heatmapReloadRunnable;
|
private Runnable heatmapReloadRunnable;
|
||||||
|
private boolean suppressCenterToggle;
|
||||||
|
private boolean mapGestureActive;
|
||||||
private float touchDownX;
|
private float touchDownX;
|
||||||
private float touchDownY;
|
private float touchDownY;
|
||||||
private Runnable pendingFitRunnable;
|
private Runnable pendingFitRunnable;
|
||||||
@@ -185,28 +190,19 @@ public class MapFragment extends Fragment {
|
|||||||
btnPairedTrack = view.findViewById(R.id.btnPairedTrack);
|
btnPairedTrack = view.findViewById(R.id.btnPairedTrack);
|
||||||
btnFindHill = view.findViewById(R.id.btnFindHill);
|
btnFindHill = view.findViewById(R.id.btnFindHill);
|
||||||
btnHeatmap = view.findViewById(R.id.btnHeatmap);
|
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);
|
mapHeatmapRadius = view.findViewById(R.id.mapHeatmapRadius);
|
||||||
mapHeatmapStatus = view.findViewById(R.id.mapHeatmapStatus);
|
mapHeatmapStatus = view.findViewById(R.id.mapHeatmapStatus);
|
||||||
mapHeatmapLegend = view.findViewById(R.id.mapHeatmapLegend);
|
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);
|
trackSpinner = view.findViewById(R.id.trackSpinner);
|
||||||
pollHelper = new FragmentPollHelper(this, this::refreshDevices);
|
pollHelper = new FragmentPollHelper(this, this::refreshDevices);
|
||||||
|
|
||||||
if (btnCenterMe != null) {
|
setupToolRail();
|
||||||
btnCenterMe.setOnClickListener(v -> centerOnSelf());
|
setupCenterModeToggle();
|
||||||
}
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
if (btnFindHill != null) {
|
if (btnFindHill != null) {
|
||||||
btnFindHill.setOnClickListener(v -> findNearestHill());
|
btnFindHill.setOnClickListener(v -> findNearestHill());
|
||||||
}
|
}
|
||||||
@@ -234,6 +230,67 @@ public class MapFragment extends Fragment {
|
|||||||
setupTrackSpinnerListener();
|
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() {
|
private void setupTrackSpinnerListener() {
|
||||||
trackSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
trackSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||||
@Override
|
@Override
|
||||||
@@ -252,12 +309,13 @@ public class MapFragment extends Fragment {
|
|||||||
|
|
||||||
private void setupMapView() {
|
private void setupMapView() {
|
||||||
mapView.setClickable(true);
|
mapView.setClickable(true);
|
||||||
mapView.getMapScaleBar().setVisible(true);
|
mapView.getMapScaleBar().setVisible(false);
|
||||||
mapView.setBuiltInZoomControls(false);
|
mapView.setBuiltInZoomControls(false);
|
||||||
|
|
||||||
mapView.setOnTouchListener((v, event) -> {
|
mapView.setOnTouchListener((v, event) -> {
|
||||||
int action = event.getActionMasked();
|
int action = event.getActionMasked();
|
||||||
if (action == MotionEvent.ACTION_DOWN) {
|
if (action == MotionEvent.ACTION_DOWN) {
|
||||||
|
mapGestureActive = true;
|
||||||
touchDownX = event.getX();
|
touchDownX = event.getX();
|
||||||
touchDownY = event.getY();
|
touchDownY = event.getY();
|
||||||
v.getParent().requestDisallowInterceptTouchEvent(true);
|
v.getParent().requestDisallowInterceptTouchEvent(true);
|
||||||
@@ -270,9 +328,11 @@ public class MapFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
} else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
} else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
||||||
v.getParent().requestDisallowInterceptTouchEvent(false);
|
v.getParent().requestDisallowInterceptTouchEvent(false);
|
||||||
|
mapGestureActive = false;
|
||||||
if (userMovedMap) {
|
if (userMovedMap) {
|
||||||
saveCameraState();
|
saveCameraState();
|
||||||
}
|
}
|
||||||
|
requestMapInvalidate();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
@@ -282,7 +342,7 @@ public class MapFragment extends Fragment {
|
|||||||
requireContext(),
|
requireContext(),
|
||||||
"loratester-tiles",
|
"loratester-tiles",
|
||||||
TILE_SIZE_PX,
|
TILE_SIZE_PX,
|
||||||
1f,
|
2.5f,
|
||||||
mapView.getModel().frameBufferModel.getOverdrawFactor()
|
mapView.getModel().frameBufferModel.getOverdrawFactor()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -384,6 +444,10 @@ public class MapFragment extends Fragment {
|
|||||||
iconLora = null;
|
iconLora = null;
|
||||||
btnFindHill = null;
|
btnFindHill = null;
|
||||||
btnHeatmap = null;
|
btnHeatmap = null;
|
||||||
|
btnToolCenter = null;
|
||||||
|
btnToolMore = null;
|
||||||
|
mapToolDrawer = null;
|
||||||
|
mapCenterMode = null;
|
||||||
mapHeatmapRadius = null;
|
mapHeatmapRadius = null;
|
||||||
mapHeatmapStatus = null;
|
mapHeatmapStatus = null;
|
||||||
mapHeatmapLegend = null;
|
mapHeatmapLegend = null;
|
||||||
@@ -393,11 +457,8 @@ public class MapFragment extends Fragment {
|
|||||||
mapStatus = null;
|
mapStatus = null;
|
||||||
mapDistance = null;
|
mapDistance = null;
|
||||||
trackStatus = null;
|
trackStatus = null;
|
||||||
btnCenterMe = null;
|
|
||||||
btnCenterTx = null;
|
|
||||||
btnCenterRx = null;
|
|
||||||
btnCenterBoth = null;
|
|
||||||
btnTrack = null;
|
btnTrack = null;
|
||||||
|
btnPairedTrack = null;
|
||||||
trackSpinner = null;
|
trackSpinner = null;
|
||||||
pollHelper = null;
|
pollHelper = null;
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
@@ -434,7 +495,9 @@ public class MapFragment extends Fragment {
|
|||||||
if (!isAdded() || btnTrack == null) {
|
if (!isAdded() || btnTrack == null) {
|
||||||
return;
|
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) {
|
if (trackStatus != null) {
|
||||||
trackStatus.setText(getString(R.string.track_status, pointCount));
|
trackStatus.setText(getString(R.string.track_status, pointCount));
|
||||||
}
|
}
|
||||||
@@ -494,9 +557,27 @@ public class MapFragment extends Fragment {
|
|||||||
} else {
|
} else {
|
||||||
liveTrackMarker.setLatLong(pos);
|
liveTrackMarker.setLatLong(pos);
|
||||||
}
|
}
|
||||||
|
requestMapInvalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestMapInvalidate() {
|
||||||
|
if (!isMapReady() || mapGestureActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
mapView.invalidate();
|
mapView.invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void requestMapInvalidateAfterLayout() {
|
||||||
|
if (!isMapReady()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mapView.post(() -> {
|
||||||
|
if (!mapGestureActive) {
|
||||||
|
mapView.invalidate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void clearLiveTrackLayers() {
|
private void clearLiveTrackLayers() {
|
||||||
liveTrackPoints.clear();
|
liveTrackPoints.clear();
|
||||||
if (mapView != null) {
|
if (mapView != null) {
|
||||||
@@ -889,7 +970,7 @@ public class MapFragment extends Fragment {
|
|||||||
bounds.add(new LatLong(fromLat, fromLon));
|
bounds.add(new LatLong(fromLat, fromLon));
|
||||||
bounds.add(hillPos);
|
bounds.add(hillPos);
|
||||||
fitBoundsOnce(bounds, false, true);
|
fitBoundsOnce(bounds, false, true);
|
||||||
mapView.invalidate();
|
requestMapInvalidateAfterLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearHillLayers() {
|
private void clearHillLayers() {
|
||||||
@@ -987,13 +1068,16 @@ public class MapFragment extends Fragment {
|
|||||||
|
|
||||||
/** Finer step for small radius, coarser for large (meters). */
|
/** Finer step for small radius, coarser for large (meters). */
|
||||||
private static int heatmapStepForRadius(int radiusM) {
|
private static int heatmapStepForRadius(int radiusM) {
|
||||||
|
if (radiusM <= 50) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
if (radiusM <= 100) {
|
if (radiusM <= 100) {
|
||||||
return 8;
|
return 1;
|
||||||
}
|
}
|
||||||
if (radiusM <= 200) {
|
if (radiusM <= 200) {
|
||||||
return 12;
|
return 3;
|
||||||
}
|
}
|
||||||
return 18;
|
return 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadHeatmap(double lat, double lon, int radiusM) {
|
private void loadHeatmap(double lat, double lon, int radiusM) {
|
||||||
@@ -1044,7 +1128,7 @@ public class MapFragment extends Fragment {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
if (mapView != null) {
|
if (mapView != null) {
|
||||||
mapView.invalidate();
|
requestMapInvalidate();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -1208,39 +1292,43 @@ public class MapFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void centerOnSelf() {
|
private boolean centerOnSelf() {
|
||||||
if (uploader == null) {
|
if (uploader == null) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
String myId = uploader.getDeviceId();
|
String myId = uploader.getDeviceId();
|
||||||
for (DeviceInfo d : lastDevices) {
|
for (DeviceInfo d : lastDevices) {
|
||||||
if (myId.equals(d.device_id) && GeoUtils.isValidCoordinate(d.lat, d.lon)) {
|
if (myId.equals(d.device_id) && GeoUtils.isValidCoordinate(d.lat, d.lon)) {
|
||||||
centerOnPoint(new LatLong(d.lat, d.lon), (byte) 14);
|
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) {
|
for (DeviceInfo d : lastDevices) {
|
||||||
if (role.equals(d.role) && GeoUtils.isValidCoordinate(d.lat, d.lon)) {
|
if (role.equals(d.role) && GeoUtils.isValidCoordinate(d.lat, d.lon)) {
|
||||||
centerOnPoint(new LatLong(d.lat, d.lon), (byte) 14);
|
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<>();
|
List<LatLong> points = new ArrayList<>();
|
||||||
for (DeviceInfo d : lastDevices) {
|
for (DeviceInfo d : lastDevices) {
|
||||||
if (GeoUtils.isValidCoordinate(d.lat, d.lon)) {
|
if (GeoUtils.isValidCoordinate(d.lat, d.lon)) {
|
||||||
points.add(new LatLong(d.lat, d.lon));
|
points.add(new LatLong(d.lat, d.lon));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!points.isEmpty()) {
|
if (points.isEmpty()) {
|
||||||
userMovedMap = false;
|
return false;
|
||||||
fitBoundsOnce(points, points.size() == 1, true);
|
|
||||||
}
|
}
|
||||||
|
userMovedMap = false;
|
||||||
|
fitBoundsOnce(points, points.size() == 1, true);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void centerOnPoint(LatLong point, byte zoom) {
|
private void centerOnPoint(LatLong point, byte zoom) {
|
||||||
@@ -1251,7 +1339,7 @@ public class MapFragment extends Fragment {
|
|||||||
position.setCenter(point);
|
position.setCenter(point);
|
||||||
position.setZoomLevel(zoom);
|
position.setZoomLevel(zoom);
|
||||||
saveCameraState();
|
saveCameraState();
|
||||||
mapView.invalidate();
|
requestMapInvalidateAfterLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveCameraState() {
|
private void saveCameraState() {
|
||||||
@@ -1307,6 +1395,7 @@ public class MapFragment extends Fragment {
|
|||||||
position.setCenter(points.get(0));
|
position.setCenter(points.get(0));
|
||||||
position.setZoomLevel((byte) 14);
|
position.setZoomLevel((byte) 14);
|
||||||
saveCameraState();
|
saveCameraState();
|
||||||
|
requestMapInvalidateAfterLayout();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
double minLat = Double.MAX_VALUE;
|
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))));
|
byte zoom = (byte) Math.max(12, Math.min(16, Math.floor(Math.min(latZoom, lonZoom))));
|
||||||
position.setZoomLevel(zoom);
|
position.setZoomLevel(zoom);
|
||||||
saveCameraState();
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<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_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
@@ -8,175 +9,232 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
<com.grigowashere.loratester.ui.ElevationHeatmapLegendView
|
<LinearLayout
|
||||||
android:id="@+id/mapHeatmapLegend"
|
android:id="@+id/mapStatusChip"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="bottom|end"
|
android:layout_gravity="top|start"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginEnd="10dp"
|
android:layout_marginEnd="56dp"
|
||||||
android:layout_marginBottom="10dp"
|
android:background="@drawable/bg_map_panel"
|
||||||
android:elevation="4dp"
|
android:elevation="6dp"
|
||||||
android:visibility="gone" />
|
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
|
<ScrollView
|
||||||
android:id="@+id/mapSidePanel"
|
android:id="@+id/mapToolDrawer"
|
||||||
android:layout_width="152dp"
|
android:layout_width="148dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="end|top"
|
android:layout_gravity="end|top"
|
||||||
android:layout_margin="6dp"
|
android:layout_marginTop="8dp"
|
||||||
android:background="#CC0F3460"
|
android:layout_marginEnd="56dp"
|
||||||
android:elevation="4dp"
|
android:background="@drawable/bg_map_panel"
|
||||||
android:fillViewport="false"
|
android:elevation="6dp"
|
||||||
android:scrollbars="none">
|
android:scrollbars="none"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="6dp">
|
android:padding="8dp">
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/mapStatus"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="#FFFFFF"
|
android:text="@string/map_center_mode"
|
||||||
android:textSize="10sp" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/mapDistance"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="2dp"
|
|
||||||
android:textColor="#00FF88"
|
android:textColor="#00FF88"
|
||||||
android:textSize="9sp"
|
android:textSize="10sp"
|
||||||
android:visibility="gone" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<LinearLayout
|
<com.google.android.material.button.MaterialButtonToggleGroup
|
||||||
|
android:id="@+id/mapCenterMode"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:orientation="horizontal">
|
android:orientation="vertical"
|
||||||
|
app:selectionRequired="false"
|
||||||
|
app:singleSelection="true">
|
||||||
|
|
||||||
<Button
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnCenterMe"
|
android:id="@+id/centerMe"
|
||||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
|
||||||
android:minHeight="32dp"
|
android:minHeight="32dp"
|
||||||
android:text="@string/map_center_me"
|
android:text="@string/map_center_me"
|
||||||
android:textSize="10sp" />
|
android:textSize="10sp" />
|
||||||
|
|
||||||
<Button
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnCenterTx"
|
android:id="@+id/centerTx"
|
||||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="2dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:minHeight="32dp"
|
android:minHeight="32dp"
|
||||||
android:text="@string/map_center_tx"
|
android:text="@string/map_center_tx"
|
||||||
android:textSize="10sp" />
|
android:textSize="10sp" />
|
||||||
|
|
||||||
<Button
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnCenterRx"
|
android:id="@+id/centerRx"
|
||||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="2dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:minHeight="32dp"
|
android:minHeight="32dp"
|
||||||
android:text="@string/map_center_rx"
|
android:text="@string/map_center_rx"
|
||||||
android:textSize="10sp" />
|
android:textSize="10sp" />
|
||||||
|
|
||||||
<Button
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnCenterBoth"
|
android:id="@+id/centerBoth"
|
||||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="2dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:minHeight="32dp"
|
android:minHeight="32dp"
|
||||||
android:text="@string/map_center_both"
|
android:text="@string/map_center_both"
|
||||||
android:textSize="10sp" />
|
android:textSize="10sp" />
|
||||||
</LinearLayout>
|
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||||
|
|
||||||
<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" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/mapHillStatus"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="2dp"
|
android:layout_marginTop="8dp"
|
||||||
android:textColor="#FFC107"
|
android:text="@string/map_heatmap_radius"
|
||||||
android:textSize="9sp"
|
android:textColor="#CCCCCC"
|
||||||
android:visibility="gone" />
|
android:textSize="9sp" />
|
||||||
|
|
||||||
<Spinner
|
<Spinner
|
||||||
android:id="@+id/mapHeatmapRadius"
|
android:id="@+id/mapHeatmapRadius"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp" />
|
android:layout_marginTop="2dp" />
|
||||||
|
|
||||||
<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" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/mapHeatmapStatus"
|
android:id="@+id/mapHeatmapStatus"
|
||||||
@@ -187,33 +245,29 @@
|
|||||||
android:textSize="9sp"
|
android:textSize="9sp"
|
||||||
android:visibility="gone" />
|
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
|
<TextView
|
||||||
android:id="@+id/mapLegend"
|
android:id="@+id/mapLegend"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="2dp"
|
android:layout_marginTop="6dp"
|
||||||
android:text="@string/map_legend"
|
android:text="@string/map_legend"
|
||||||
android:textColor="#CCCCCC"
|
android:textColor="#CCCCCC"
|
||||||
android:textSize="9sp" />
|
android:textSize="9sp" />
|
||||||
|
|
||||||
<Button
|
<Spinner
|
||||||
android:id="@+id/btnTrack"
|
android:id="@+id/trackSpinner"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="6dp" />
|
||||||
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" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/trackStatus"
|
android:id="@+id/trackStatus"
|
||||||
@@ -222,13 +276,18 @@
|
|||||||
android:layout_marginTop="2dp"
|
android:layout_marginTop="2dp"
|
||||||
android:textColor="#CCCCCC"
|
android:textColor="#CCCCCC"
|
||||||
android:textSize="9sp" />
|
android:textSize="9sp" />
|
||||||
|
|
||||||
<Spinner
|
|
||||||
android:id="@+id/trackSpinner"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="4dp" />
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</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>
|
</FrameLayout>
|
||||||
|
|||||||
@@ -80,6 +80,12 @@
|
|||||||
<string name="map_center_tx">TX</string>
|
<string name="map_center_tx">TX</string>
|
||||||
<string name="map_center_rx">RX</string>
|
<string name="map_center_rx">RX</string>
|
||||||
<string name="map_center_both">Оба</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="map_gps_distance">GPS между устройствами: %1$s m</string>
|
||||||
<string name="status_server">Связь с сервером</string>
|
<string name="status_server">Связь с сервером</string>
|
||||||
<string name="status_server_short">сервер</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` внутри контейнера).
|
БД хранится в volume `loratester-data` (`/data/loratester.db` внутри контейнера).
|
||||||
|
|
||||||
## Деплой (grigowashere.ru:7634)
|
## Деплой (lora.grigowashere.ru)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /srv/storage/disk2/services/LoraTester/server
|
cd /srv/storage/disk2/services/LoraTester/server
|
||||||
@@ -148,6 +148,6 @@ python -m pytest tests/ -v
|
|||||||
|
|
||||||
## Android
|
## 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 на устройстве.
|
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 = 2500
|
||||||
|
_MAX_GRID_POINTS_FINE = 12000
|
||||||
|
|
||||||
|
|
||||||
def _auto_step_m(radius_m: float) -> float:
|
def _auto_step_m(radius_m: float) -> float:
|
||||||
@@ -401,11 +402,15 @@ def _sample_circular_grid(
|
|||||||
return cells
|
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:
|
if step_m <= 0:
|
||||||
step_m = _auto_step_m(radius_m)
|
step_m = _auto_step_m(radius_m)
|
||||||
step_m = max(5.0, min(float(step_m), 100.0))
|
min_step = 1.0 if radius_m <= 100.0 else 5.0
|
||||||
while len(_sample_circular_grid(lat, lon, radius_m, step_m)) > _MAX_GRID_POINTS:
|
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)
|
step_m = math.ceil(step_m * 1.25)
|
||||||
if step_m >= radius_m:
|
if step_m >= radius_m:
|
||||||
break
|
break
|
||||||
@@ -427,7 +432,7 @@ def build_elevation_grid(
|
|||||||
"elevation_url": ELEVATION_API_URL,
|
"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)
|
step_m = _resolve_grid_step(lat, lon, radius_m, step_m)
|
||||||
|
|
||||||
center_elev = fetch_elevation_m(lat, lon)
|
center_elev = fetch_elevation_m(lat, lon)
|
||||||
|
|||||||
@@ -349,8 +349,8 @@ def elevation_nearest_hill(
|
|||||||
def elevation_grid(
|
def elevation_grid(
|
||||||
lat: float = Query(..., ge=-90.0, le=90.0),
|
lat: float = Query(..., ge=-90.0, le=90.0),
|
||||||
lon: float = Query(..., ge=-180.0, le=180.0),
|
lon: float = Query(..., ge=-180.0, le=180.0),
|
||||||
radius_m: float = Query(200.0, ge=100.0, le=500.0),
|
radius_m: float = Query(200.0, ge=50.0, le=500.0),
|
||||||
step_m: float = Query(0.0, ge=0.0, le=100.0),
|
step_m: float = Query(0.0, ge=1.0, le=100.0),
|
||||||
):
|
):
|
||||||
from core.elevation import build_elevation_grid
|
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"])
|
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):
|
def test_build_elevation_grid_limits_points(monkeypatch):
|
||||||
monkeypatch.setattr(elev, "_probe_checked_at", 0.0)
|
monkeypatch.setattr(elev, "_probe_checked_at", 0.0)
|
||||||
monkeypatch.setattr(elev, "probe_elevation_api", lambda force=False: {"ok": True, "error": None})
|
monkeypatch.setattr(elev, "probe_elevation_api", lambda force=False: {"ok": True, "error": None})
|
||||||
|
|||||||
Reference in New Issue
Block a user