mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-14 22:55:12 +10:00
Merge pull request #416 from zjs81/dev-DesktopMapControls
Add desktop map controls
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
@@ -304,6 +304,8 @@ class ChannelMessagePathMapScreen extends StatefulWidget {
|
||||
class _ChannelMessagePathMapScreenState
|
||||
extends State<ChannelMessagePathMapScreen> {
|
||||
static const double _labelZoomThreshold = 8.5;
|
||||
static const double _mapMinZoom = 2.0;
|
||||
static const double _mapMaxZoom = 18.0;
|
||||
|
||||
final MapController _mapController = MapController();
|
||||
Uint8List? _selectedPath;
|
||||
@@ -330,6 +332,18 @@ class _ChannelMessagePathMapScreenState
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_mapController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _isDesktopPlatform(TargetPlatform platform) {
|
||||
return platform == TargetPlatform.linux ||
|
||||
platform == TargetPlatform.windows ||
|
||||
platform == TargetPlatform.macOS;
|
||||
}
|
||||
|
||||
double _getPathDistance(List<LatLng> points) {
|
||||
double totalDistance = 0.0;
|
||||
final distanceCalculator = Distance();
|
||||
@@ -357,6 +371,70 @@ class _ChannelMessagePathMapScreenState
|
||||
});
|
||||
}
|
||||
|
||||
void _zoomMapBy(double delta) {
|
||||
final camera = _mapController.camera;
|
||||
final nextZoom = (camera.zoom + delta)
|
||||
.clamp(_mapMinZoom, _mapMaxZoom)
|
||||
.toDouble();
|
||||
_mapController.move(camera.center, nextZoom);
|
||||
}
|
||||
|
||||
void _resetMapView({
|
||||
required LatLng initialCenter,
|
||||
required double initialZoom,
|
||||
required LatLngBounds? bounds,
|
||||
}) {
|
||||
if (bounds != null) {
|
||||
_mapController.fitCamera(
|
||||
CameraFit.bounds(
|
||||
bounds: bounds,
|
||||
padding: const EdgeInsets.all(64),
|
||||
maxZoom: 16,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
_mapController.move(initialCenter, initialZoom);
|
||||
}
|
||||
|
||||
Widget _buildDesktopMapControls({
|
||||
required LatLng initialCenter,
|
||||
required double initialZoom,
|
||||
required LatLngBounds? bounds,
|
||||
}) {
|
||||
return Positioned(
|
||||
left: 16,
|
||||
top: 16,
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Zoom in',
|
||||
onPressed: () => _zoomMapBy(1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: 'Zoom out',
|
||||
onPressed: () => _zoomMapBy(-1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.my_location),
|
||||
tooltip: 'Center map',
|
||||
onPressed: () => _resetMapView(
|
||||
initialCenter: initialCenter,
|
||||
initialZoom: initialZoom,
|
||||
bounds: bounds,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<MeshCoreConnector>(
|
||||
@@ -372,6 +450,7 @@ class _ChannelMessagePathMapScreenState
|
||||
primaryPath,
|
||||
widget.message.pathVariants,
|
||||
);
|
||||
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||
final selectedPathTmp = _resolveSelectedPath(
|
||||
_selectedPath,
|
||||
observedPaths,
|
||||
@@ -451,10 +530,20 @@ class _ChannelMessagePathMapScreenState
|
||||
padding: const EdgeInsets.all(64),
|
||||
maxZoom: 16,
|
||||
),
|
||||
minZoom: 2.0,
|
||||
maxZoom: 18.0,
|
||||
minZoom: _mapMinZoom,
|
||||
maxZoom: _mapMaxZoom,
|
||||
interactionOptions: InteractionOptions(
|
||||
flags: ~InteractiveFlag.rotate,
|
||||
scrollWheelVelocity: isDesktop ? 0.012 : 0.005,
|
||||
cursorKeyboardRotationOptions:
|
||||
CursorKeyboardRotationOptions.disabled(),
|
||||
keyboardOptions: isDesktop
|
||||
? const KeyboardOptions(
|
||||
enableArrowKeysPanning: true,
|
||||
enableWASDPanning: true,
|
||||
enableRFZooming: true,
|
||||
)
|
||||
: const KeyboardOptions.disabled(),
|
||||
),
|
||||
onPositionChanged: (camera, hasGesture) {
|
||||
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||
@@ -486,6 +575,12 @@ class _ChannelMessagePathMapScreenState
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
_buildDesktopMapControls(
|
||||
initialCenter: initialCenter,
|
||||
initialZoom: initialZoom,
|
||||
bounds: bounds,
|
||||
),
|
||||
if (observedPaths.length > 1)
|
||||
_buildPathSelector(context, observedPaths, selectedIndex, (
|
||||
index,
|
||||
|
||||
@@ -492,8 +492,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
final unread =
|
||||
connector.getUnreadCountForChannelIndex(channel.index);
|
||||
final unread = connector.getUnreadCountForChannelIndex(
|
||||
channel.index,
|
||||
);
|
||||
connector.markChannelRead(channel.index);
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
if (context.mounted) {
|
||||
@@ -1497,7 +1498,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.channels_channelUpdateFailed('$e')),
|
||||
content: Text(
|
||||
context.l10n.channels_channelUpdateFailed('$e'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1218,8 +1218,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow(context.l10n.chat_type, contact.typeLabel(context.l10n)),
|
||||
_buildInfoRow(context.l10n.chat_path, contact.pathLabel(context.l10n)),
|
||||
_buildInfoRow(
|
||||
context.l10n.chat_type,
|
||||
contact.typeLabel(context.l10n),
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.chat_path,
|
||||
contact.pathLabel(context.l10n),
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.contact_lastSeen,
|
||||
_formatContactLastMessage(contact.lastMessageAt),
|
||||
|
||||
@@ -932,16 +932,15 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
_showRoomLogin(context, contact, RoomLoginDestination.chat);
|
||||
} else {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final unread =
|
||||
connector.getUnreadCountForContactKey(contact.publicKeyHex);
|
||||
final unread = connector.getUnreadCountForContactKey(
|
||||
contact.publicKeyHex,
|
||||
);
|
||||
connector.markContactRead(contact.publicKeyHex);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChatScreen(
|
||||
contact: contact,
|
||||
initialUnreadCount: unread,
|
||||
),
|
||||
builder: (context) =>
|
||||
ChatScreen(contact: contact, initialUnreadCount: unread),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -998,8 +997,9 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
room: room,
|
||||
onLogin: (password, isAdmin) {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final unread =
|
||||
connector.getUnreadCountForContactKey(room.publicKeyHex);
|
||||
final unread = connector.getUnreadCountForContactKey(
|
||||
room.publicKeyHex,
|
||||
);
|
||||
connector.markContactRead(room.publicKeyHex);
|
||||
Navigator.push(
|
||||
context,
|
||||
@@ -1011,10 +1011,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
password: password,
|
||||
isAdmin: isAdmin,
|
||||
)
|
||||
: ChatScreen(
|
||||
contact: room,
|
||||
initialUnreadCount: unread,
|
||||
),
|
||||
: ChatScreen(contact: room, initialUnreadCount: unread),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
@@ -56,8 +57,11 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
static const double _maxAntennaFeet = 400.0;
|
||||
static const double _maxAntennaMeters = _maxAntennaFeet / _metersToFeet;
|
||||
static const double _labelZoomThreshold = 8.5;
|
||||
static const double _mapMinZoom = 2.0;
|
||||
static const double _mapMaxZoom = 18.0;
|
||||
|
||||
final LineOfSightService _lineOfSightService = LineOfSightService();
|
||||
final MapController _mapController = MapController();
|
||||
|
||||
bool _loading = false;
|
||||
String? _error;
|
||||
@@ -99,10 +103,85 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_mapController.dispose();
|
||||
_lineOfSightService.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _isDesktopPlatform(TargetPlatform platform) {
|
||||
return platform == TargetPlatform.linux ||
|
||||
platform == TargetPlatform.windows ||
|
||||
platform == TargetPlatform.macOS;
|
||||
}
|
||||
|
||||
void _zoomMapBy(double delta) {
|
||||
final camera = _mapController.camera;
|
||||
final nextZoom = (camera.zoom + delta)
|
||||
.clamp(_mapMinZoom, _mapMaxZoom)
|
||||
.toDouble();
|
||||
_mapController.move(camera.center, nextZoom);
|
||||
}
|
||||
|
||||
void _resetMapView({
|
||||
required LatLng initialCenter,
|
||||
required double initialZoom,
|
||||
required LatLngBounds? bounds,
|
||||
}) {
|
||||
if (bounds != null) {
|
||||
_mapController.fitCamera(
|
||||
CameraFit.bounds(
|
||||
bounds: bounds,
|
||||
padding: const EdgeInsets.all(64),
|
||||
maxZoom: 16,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
_mapController.move(initialCenter, initialZoom);
|
||||
}
|
||||
|
||||
Widget _buildDesktopMapControls({
|
||||
required LatLng initialCenter,
|
||||
required double initialZoom,
|
||||
required LatLngBounds? bounds,
|
||||
}) {
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
final topOffset = _showHud
|
||||
? math.min(screenHeight * 0.52 + 24, screenHeight - 220)
|
||||
: 12.0;
|
||||
return Positioned(
|
||||
top: topOffset,
|
||||
left: 12,
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Zoom in',
|
||||
onPressed: () => _zoomMapBy(1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: 'Zoom out',
|
||||
onPressed: () => _zoomMapBy(-1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.my_location),
|
||||
tooltip: 'Center map',
|
||||
onPressed: () => _resetMapView(
|
||||
initialCenter: initialCenter,
|
||||
initialZoom: initialZoom,
|
||||
bounds: bounds,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _runLos() async {
|
||||
final start = _start;
|
||||
final end = _end;
|
||||
@@ -325,6 +404,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
? LatLngBounds.fromPoints(mapPoints)
|
||||
: null;
|
||||
final initialZoom = mapPoints.length > 1 ? 13.0 : 2.0;
|
||||
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||
if (!_didReceivePositionUpdate) {
|
||||
_showMarkerLabels = initialZoom >= _labelZoomThreshold;
|
||||
}
|
||||
@@ -350,6 +430,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
body: Stack(
|
||||
children: [
|
||||
FlutterMap(
|
||||
mapController: _mapController,
|
||||
options: MapOptions(
|
||||
initialCenter: initialCenter,
|
||||
initialZoom: initialZoom,
|
||||
@@ -362,7 +443,19 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
),
|
||||
interactionOptions: InteractionOptions(
|
||||
flags: ~InteractiveFlag.rotate,
|
||||
scrollWheelVelocity: isDesktop ? 0.012 : 0.005,
|
||||
cursorKeyboardRotationOptions:
|
||||
CursorKeyboardRotationOptions.disabled(),
|
||||
keyboardOptions: isDesktop
|
||||
? const KeyboardOptions(
|
||||
enableArrowKeysPanning: true,
|
||||
enableWASDPanning: true,
|
||||
enableRFZooming: true,
|
||||
)
|
||||
: const KeyboardOptions.disabled(),
|
||||
),
|
||||
minZoom: _mapMinZoom,
|
||||
maxZoom: _mapMaxZoom,
|
||||
onLongPress: (_, point) => _addCustomPoint(point),
|
||||
onPositionChanged: (camera, hasGesture) {
|
||||
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||
@@ -389,6 +482,12 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
_buildDesktopMapControls(
|
||||
initialCenter: initialCenter,
|
||||
initialZoom: initialZoom,
|
||||
bounds: bounds,
|
||||
),
|
||||
if (_showHud)
|
||||
Positioned(
|
||||
left: 12,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
@@ -18,6 +19,9 @@ class MapCacheScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
static const double _mapMinZoom = 2.0;
|
||||
static const double _mapMaxZoom = 18.0;
|
||||
|
||||
final MapController _mapController = MapController();
|
||||
|
||||
LatLngBounds? _selectedBounds;
|
||||
@@ -43,6 +47,61 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _isDesktopPlatform(TargetPlatform platform) {
|
||||
return platform == TargetPlatform.linux ||
|
||||
platform == TargetPlatform.windows ||
|
||||
platform == TargetPlatform.macOS;
|
||||
}
|
||||
|
||||
void _zoomMapBy(double delta) {
|
||||
final camera = _mapController.camera;
|
||||
final nextZoom = (camera.zoom + delta)
|
||||
.clamp(_mapMinZoom, _mapMaxZoom)
|
||||
.toDouble();
|
||||
_mapController.move(camera.center, nextZoom);
|
||||
}
|
||||
|
||||
void _resetMapView() {
|
||||
final bounds = _selectedBounds;
|
||||
if (bounds != null) {
|
||||
_mapController.fitCamera(
|
||||
CameraFit.bounds(bounds: bounds, padding: const EdgeInsets.all(48)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
_mapController.move(const LatLng(0, 0), 2.0);
|
||||
}
|
||||
|
||||
Widget _buildDesktopMapControls() {
|
||||
return Positioned(
|
||||
top: 12,
|
||||
left: 12,
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Zoom in',
|
||||
onPressed: () => _zoomMapBy(1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: 'Zoom out',
|
||||
onPressed: () => _zoomMapBy(-1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.my_location),
|
||||
tooltip: 'Center map',
|
||||
onPressed: _resetMapView,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _loadSettings() {
|
||||
final settings = context.read<AppSettingsService>().settings;
|
||||
final bounds = MapTileCacheService.boundsFromJson(settings.mapCacheBounds);
|
||||
@@ -222,6 +281,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
final tileCache = context.read<MapTileCacheService>();
|
||||
final selectedBounds = _selectedBounds;
|
||||
final l10n = context.l10n;
|
||||
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||
final progressValue = _estimatedTiles == 0
|
||||
? 0.0
|
||||
: (_completedTiles / _estimatedTiles).clamp(0.0, 1.0).toDouble();
|
||||
@@ -238,11 +298,24 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
children: [
|
||||
FlutterMap(
|
||||
mapController: _mapController,
|
||||
options: const MapOptions(
|
||||
initialCenter: LatLng(0, 0),
|
||||
options: MapOptions(
|
||||
initialCenter: const LatLng(0, 0),
|
||||
initialZoom: 2.0,
|
||||
minZoom: 2.0,
|
||||
maxZoom: 18.0,
|
||||
minZoom: _mapMinZoom,
|
||||
maxZoom: _mapMaxZoom,
|
||||
interactionOptions: InteractionOptions(
|
||||
flags: ~InteractiveFlag.rotate,
|
||||
scrollWheelVelocity: isDesktop ? 0.012 : 0.005,
|
||||
cursorKeyboardRotationOptions:
|
||||
CursorKeyboardRotationOptions.disabled(),
|
||||
keyboardOptions: isDesktop
|
||||
? const KeyboardOptions(
|
||||
enableArrowKeysPanning: true,
|
||||
enableWASDPanning: true,
|
||||
enableRFZooming: true,
|
||||
)
|
||||
: const KeyboardOptions.disabled(),
|
||||
),
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
@@ -265,6 +338,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isDesktop) _buildDesktopMapControls(),
|
||||
Positioned(
|
||||
top: 12,
|
||||
right: 12,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -58,6 +57,8 @@ class MapScreen extends StatefulWidget {
|
||||
class _MapScreenState extends State<MapScreen> {
|
||||
// Zoom level at which node labels start to appear
|
||||
static const double _labelZoomThreshold = 14.0;
|
||||
static const double _mapMinZoom = 2.0;
|
||||
static const double _mapMaxZoom = 18.0;
|
||||
|
||||
final MapController _mapController = MapController();
|
||||
final MapMarkerService _markerService = MapMarkerService();
|
||||
@@ -150,11 +151,62 @@ class _MapScreenState extends State<MapScreen> {
|
||||
return zoom.clamp(4.0, 15.0);
|
||||
}
|
||||
|
||||
bool _isDesktopPlatform(TargetPlatform platform) {
|
||||
return platform == TargetPlatform.linux ||
|
||||
platform == TargetPlatform.windows ||
|
||||
platform == TargetPlatform.macOS;
|
||||
}
|
||||
|
||||
void _zoomMapBy(double delta) {
|
||||
final camera = _mapController.camera;
|
||||
final nextZoom = (camera.zoom + delta)
|
||||
.clamp(_mapMinZoom, _mapMaxZoom)
|
||||
.toDouble();
|
||||
_mapController.move(camera.center, nextZoom);
|
||||
}
|
||||
|
||||
Widget _buildDesktopMapControls(
|
||||
BuildContext context, {
|
||||
required LatLng center,
|
||||
required double zoom,
|
||||
required bool hasPathSelector,
|
||||
}) {
|
||||
return Positioned(
|
||||
left: 16,
|
||||
top: hasPathSelector ? null : 16,
|
||||
bottom: hasPathSelector ? 16 : null,
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Zoom in',
|
||||
onPressed: () => _zoomMapBy(1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: 'Zoom out',
|
||||
onPressed: () => _zoomMapBy(-1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.my_location),
|
||||
tooltip: 'Center map',
|
||||
onPressed: () => _mapController.move(center, zoom),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer3<MeshCoreConnector, AppSettingsService, PathHistoryService>(
|
||||
builder: (context, connector, settingsService, pathHistory, child) {
|
||||
final tileCache = context.read<MapTileCacheService>();
|
||||
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||
final settings = settingsService.settings;
|
||||
final allContacts = connector.allContacts;
|
||||
|
||||
@@ -451,10 +503,20 @@ class _MapScreenState extends State<MapScreen> {
|
||||
options: MapOptions(
|
||||
initialCenter: center,
|
||||
initialZoom: initialZoom,
|
||||
minZoom: 2.0,
|
||||
maxZoom: 18.0,
|
||||
minZoom: _mapMinZoom,
|
||||
maxZoom: _mapMaxZoom,
|
||||
interactionOptions: InteractionOptions(
|
||||
flags: ~InteractiveFlag.rotate,
|
||||
scrollWheelVelocity: isDesktop ? 0.012 : 0.005,
|
||||
cursorKeyboardRotationOptions:
|
||||
CursorKeyboardRotationOptions.disabled(),
|
||||
keyboardOptions: isDesktop
|
||||
? const KeyboardOptions(
|
||||
enableArrowKeysPanning: true,
|
||||
enableWASDPanning: true,
|
||||
enableRFZooming: true,
|
||||
)
|
||||
: const KeyboardOptions.disabled(),
|
||||
),
|
||||
onTap: (_, latLng) {
|
||||
if (_isSelectingPoi) {
|
||||
@@ -598,6 +660,13 @@ class _MapScreenState extends State<MapScreen> {
|
||||
sharedMarkers.length,
|
||||
guessedLocations.length,
|
||||
),
|
||||
if (isDesktop)
|
||||
_buildDesktopMapControls(
|
||||
context,
|
||||
center: center,
|
||||
zoom: initialZoom,
|
||||
hasPathSelector: _isBuildingPathTrace,
|
||||
),
|
||||
if (_isBuildingPathTrace) _buildPathTraceOverlay(),
|
||||
],
|
||||
),
|
||||
@@ -1480,8 +1549,14 @@ class _MapScreenState extends State<MapScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow(context.l10n.map_type, contact.typeLabel(context.l10n)),
|
||||
_buildInfoRow(context.l10n.map_path, contact.pathLabel(context.l10n)),
|
||||
_buildInfoRow(
|
||||
context.l10n.map_type,
|
||||
contact.typeLabel(context.l10n),
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.map_path,
|
||||
contact.pathLabel(context.l10n),
|
||||
),
|
||||
if (contact.hasLocation)
|
||||
_buildInfoRow(
|
||||
context.l10n.map_location,
|
||||
|
||||
@@ -76,9 +76,12 @@ class PathTraceMapScreen extends StatefulWidget {
|
||||
|
||||
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
static const double _labelZoomThreshold = 8.5;
|
||||
static const double _mapMinZoom = 2.0;
|
||||
static const double _mapMaxZoom = 18.0;
|
||||
//miles to meters conversion for filtering out repeaters that are too far from the last known GPS hop to be a likely match, to avoid false matches that throw off the inferred positions of other hops in the path
|
||||
static const double _maxRepeaterMatchDistanceMeters = 40 * 1609.344;
|
||||
|
||||
final MapController _mapController = MapController();
|
||||
StreamSubscription<Uint8List>? _frameSubscription;
|
||||
Timer? _timeoutTimer;
|
||||
|
||||
@@ -116,11 +119,74 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_mapController.dispose();
|
||||
_frameSubscription?.cancel();
|
||||
_timeoutTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _isDesktopPlatform(TargetPlatform platform) {
|
||||
return platform == TargetPlatform.linux ||
|
||||
platform == TargetPlatform.windows ||
|
||||
platform == TargetPlatform.macOS;
|
||||
}
|
||||
|
||||
void _zoomMapBy(double delta) {
|
||||
final camera = _mapController.camera;
|
||||
final nextZoom = (camera.zoom + delta)
|
||||
.clamp(_mapMinZoom, _mapMaxZoom)
|
||||
.toDouble();
|
||||
_mapController.move(camera.center, nextZoom);
|
||||
}
|
||||
|
||||
void _resetMapView() {
|
||||
final bounds = _bounds;
|
||||
if (bounds != null) {
|
||||
_mapController.fitCamera(
|
||||
CameraFit.bounds(
|
||||
bounds: bounds,
|
||||
padding: const EdgeInsets.all(64),
|
||||
maxZoom: 16,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final center = _initialCenter;
|
||||
if (center != null) {
|
||||
_mapController.move(center, _initialZoom);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDesktopMapControls() {
|
||||
return Positioned(
|
||||
top: 16,
|
||||
left: 16,
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Zoom in',
|
||||
onPressed: () => _zoomMapBy(1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: 'Zoom out',
|
||||
onPressed: () => _zoomMapBy(-1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.my_location),
|
||||
tooltip: 'Center map',
|
||||
onPressed: _resetMapView,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Uint8List buildPath(Uint8List pathBytes) {
|
||||
Uint8List traceBytes;
|
||||
|
||||
@@ -519,6 +585,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
),
|
||||
if (_hasData)
|
||||
_buildMapPathTrace(context, tileCache, _targetContact),
|
||||
if (_hasData && _isDesktopPlatform(defaultTargetPlatform))
|
||||
_buildDesktopMapControls(),
|
||||
if (_points.isEmpty &&
|
||||
!_hasData &&
|
||||
!_isLoading &&
|
||||
@@ -801,10 +869,24 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
MapTileCacheService tileCache,
|
||||
Contact? target,
|
||||
) {
|
||||
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||
return FlutterMap(
|
||||
key: _mapKey,
|
||||
mapController: _mapController,
|
||||
options: MapOptions(
|
||||
interactionOptions: InteractionOptions(flags: ~InteractiveFlag.rotate),
|
||||
interactionOptions: InteractionOptions(
|
||||
flags: ~InteractiveFlag.rotate,
|
||||
scrollWheelVelocity: isDesktop ? 0.012 : 0.005,
|
||||
cursorKeyboardRotationOptions:
|
||||
CursorKeyboardRotationOptions.disabled(),
|
||||
keyboardOptions: isDesktop
|
||||
? const KeyboardOptions(
|
||||
enableArrowKeysPanning: true,
|
||||
enableWASDPanning: true,
|
||||
enableRFZooming: true,
|
||||
)
|
||||
: const KeyboardOptions.disabled(),
|
||||
),
|
||||
initialCenter: _initialCenter!,
|
||||
initialZoom: _initialZoom,
|
||||
initialCameraFit: _bounds == null
|
||||
@@ -814,8 +896,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
padding: const EdgeInsets.all(64),
|
||||
maxZoom: 16,
|
||||
),
|
||||
minZoom: 2.0,
|
||||
maxZoom: 18.0,
|
||||
minZoom: _mapMinZoom,
|
||||
maxZoom: _mapMaxZoom,
|
||||
onPositionChanged: (camera, hasGesture) {
|
||||
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||
if (shouldShow != _showNodeLabels && mounted) {
|
||||
|
||||
@@ -65,8 +65,7 @@ class BackgroundService {
|
||||
return AppLocalizations.delegate.load(overrideLocale);
|
||||
}
|
||||
}
|
||||
final preferred =
|
||||
WidgetsBinding.instance.platformDispatcher.locales;
|
||||
final preferred = WidgetsBinding.instance.platformDispatcher.locales;
|
||||
final match = basicLocaleListResolution(preferred, supported);
|
||||
return AppLocalizations.delegate.load(match);
|
||||
}
|
||||
|
||||
@@ -36,10 +36,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('repeater', () {
|
||||
expect(
|
||||
_contact(type: advTypeRepeater).typeLabel(l10n),
|
||||
'Repeater',
|
||||
);
|
||||
expect(_contact(type: advTypeRepeater).typeLabel(l10n), 'Repeater');
|
||||
});
|
||||
|
||||
test('room', () {
|
||||
@@ -57,24 +54,15 @@ void main() {
|
||||
|
||||
group('Contact.pathLabel (override)', () {
|
||||
test('override < 0 -> flood (forced)', () {
|
||||
expect(
|
||||
_contact(pathOverride: -1).pathLabel(l10n),
|
||||
'Flood (forced)',
|
||||
);
|
||||
expect(_contact(pathOverride: -1).pathLabel(l10n), 'Flood (forced)');
|
||||
});
|
||||
|
||||
test('override == 0 -> direct (forced)', () {
|
||||
expect(
|
||||
_contact(pathOverride: 0).pathLabel(l10n),
|
||||
'Direct (forced)',
|
||||
);
|
||||
expect(_contact(pathOverride: 0).pathLabel(l10n), 'Direct (forced)');
|
||||
});
|
||||
|
||||
test('override > 0 -> hops (forced)', () {
|
||||
expect(
|
||||
_contact(pathOverride: 3).pathLabel(l10n),
|
||||
'3 hops (forced)',
|
||||
);
|
||||
expect(_contact(pathOverride: 3).pathLabel(l10n), '3 hops (forced)');
|
||||
});
|
||||
|
||||
test('override takes precedence over pathLength', () {
|
||||
|
||||
Reference in New Issue
Block a user