mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Add GPX import export for trails
This commit is contained in:
@@ -27,6 +27,7 @@ import '../widgets/map_debug_info.dart';
|
||||
import '../widgets/map/compass_widget.dart';
|
||||
import '../widgets/map/detailed_compass_dialog.dart';
|
||||
import '../widgets/map/drawing_layer.dart';
|
||||
import '../widgets/map/drawing_toolbar.dart';
|
||||
import '../widgets/map/location_trail_layer.dart';
|
||||
import '../widgets/map/trail_controls.dart';
|
||||
import '../widgets/map/map_message_overlay.dart';
|
||||
@@ -2131,6 +2132,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
right: 16,
|
||||
child: Column(
|
||||
children: [
|
||||
const DrawingToolbar(),
|
||||
const SizedBox(height: 8),
|
||||
// Hide other buttons when in drawing mode
|
||||
if (!drawingProvider.isDrawing) ...[
|
||||
// Current Location - always center to GPS
|
||||
|
||||
@@ -318,11 +318,11 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
||||
final repeater = cluster.members.first;
|
||||
return flutter_map.Marker(
|
||||
point: cluster.center,
|
||||
width: 40,
|
||||
height: 40,
|
||||
width: 80,
|
||||
height: 100,
|
||||
child: GestureDetector(
|
||||
onTap: () => _showRepeaterDetails(repeater),
|
||||
child: _RepeaterMarker(color: markerColor),
|
||||
child: _RepeaterMarker(color: markerColor, label: repeater.name),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -646,25 +646,52 @@ class _LegendRow extends StatelessWidget {
|
||||
|
||||
class _RepeaterMarker extends StatelessWidget {
|
||||
final Color color;
|
||||
final String label;
|
||||
|
||||
const _RepeaterMarker({required this.color});
|
||||
const _RepeaterMarker({required this.color, required this.label});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.18),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3),
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(Icons.router, color: Colors.white, size: 18),
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: const Icon(Icons.router, color: Colors.white, size: 18),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 80),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.7),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../models/contact.dart';
|
||||
import '../../models/location_trail.dart';
|
||||
import '../../providers/map_provider.dart';
|
||||
import '../../providers/contacts_provider.dart';
|
||||
import '../../services/gpx_service.dart';
|
||||
import '../../services/trail_color_service.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
@@ -66,6 +69,31 @@ class TrailControls extends StatelessWidget {
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _importTrail(context, mapProvider),
|
||||
icon: const Icon(Icons.file_upload_outlined),
|
||||
label: Text(l10n.importFromClipboard),
|
||||
),
|
||||
if (mapProvider.currentTrail != null &&
|
||||
mapProvider.currentTrail!.points.length >= 2)
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _exportTrail(
|
||||
context,
|
||||
mapProvider.currentTrail!,
|
||||
customName: 'My Trail',
|
||||
),
|
||||
icon: const Icon(Icons.file_download_outlined),
|
||||
label: Text(l10n.exportToClipboard),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Trail stats
|
||||
if (mapProvider.currentTrail != null &&
|
||||
mapProvider.currentTrail!.points.isNotEmpty)
|
||||
@@ -104,8 +132,6 @@ class TrailControls extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Clear trail button
|
||||
if (mapProvider.currentTrail != null &&
|
||||
mapProvider.currentTrail!.points.isNotEmpty)
|
||||
@@ -238,7 +264,17 @@ class TrailControls extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
title: Text(contact.displayName),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(child: Text(contact.displayName)),
|
||||
IconButton(
|
||||
tooltip: l10n.exportToClipboard,
|
||||
icon: const Icon(Icons.file_download_outlined),
|
||||
onPressed: () =>
|
||||
_exportContactTrail(context, contact),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Text(
|
||||
'${contact.advertHistory.length} points',
|
||||
),
|
||||
@@ -266,6 +302,92 @@ class TrailControls extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _importTrail(
|
||||
BuildContext context,
|
||||
MapProvider mapProvider,
|
||||
) async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
try {
|
||||
final importedTrail = await GpxService.importTrailFromFile();
|
||||
if (importedTrail == null || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
mapProvider.replaceCurrentTrailWithImport(importedTrail);
|
||||
mapProvider.clearImportedTrail();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'${l10n.locationTrail}: ${importedTrail.points.length} ${l10n.points.toLowerCase()}',
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.importFailed(error.toString()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _exportTrail(
|
||||
BuildContext context,
|
||||
LocationTrail trail, {
|
||||
String? customName,
|
||||
}) async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
try {
|
||||
final success = await GpxService.exportTrailToFile(
|
||||
trail,
|
||||
customName: customName,
|
||||
);
|
||||
if (!success && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.exportFailed('unable to share GPX'))),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.exportFailed(error.toString()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _exportContactTrail(BuildContext context, Contact contact) async {
|
||||
await _exportTrail(
|
||||
context,
|
||||
_buildContactTrail(contact),
|
||||
customName: '${contact.displayName} Trail',
|
||||
);
|
||||
}
|
||||
|
||||
LocationTrail _buildContactTrail(Contact contact) {
|
||||
final sortedHistory = [...contact.advertHistory]
|
||||
..sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
|
||||
return LocationTrail(
|
||||
id: 'contact_${contact.publicKeyHex}',
|
||||
isActive: false,
|
||||
startTime: sortedHistory.first.timestamp,
|
||||
endTime: sortedHistory.last.timestamp,
|
||||
points: sortedHistory
|
||||
.map(
|
||||
(advert) => TrailPoint(
|
||||
position: advert.location,
|
||||
timestamp: advert.timestamp,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
void _showClearConfirmation(
|
||||
BuildContext context,
|
||||
MapProvider mapProvider,
|
||||
|
||||
Reference in New Issue
Block a user