mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: Enhance SAR message handling with improved parsing and object marker generation
This commit is contained in:
@@ -15,7 +15,9 @@
|
|||||||
"Bash(nc:*)",
|
"Bash(nc:*)",
|
||||||
"Bash(pkill:*)",
|
"Bash(pkill:*)",
|
||||||
"WebFetch(domain:github.com)",
|
"WebFetch(domain:github.com)",
|
||||||
"WebFetch(domain:raw.githubusercontent.com)"
|
"WebFetch(domain:raw.githubusercontent.com)",
|
||||||
|
"Bash(dart run:*)",
|
||||||
|
"Bash(dart test_sar_debug.dart:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
10
CLAUDE.md
10
CLAUDE.md
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
This document provides technical details for AI assistants (like Claude) working with this codebase.
|
This document provides technical details for AI assistants (like Claude) working with this codebase.
|
||||||
|
|
||||||
|
## ⚠️ IMPORTANT: Flutter Development Rules
|
||||||
|
|
||||||
|
**NEVER run or kill Flutter processes:**
|
||||||
|
- **DO NOT** execute `flutter run` command
|
||||||
|
- **DO NOT** kill Flutter processes (e.g., `pkill flutter`, `killall flutter`)
|
||||||
|
- The user manages the Flutter development server themselves
|
||||||
|
- Only make code changes and let the user trigger hot reload manually
|
||||||
|
|
||||||
|
**Hot reload happens automatically when you save files** - the user has their own Flutter process running and will see changes instantly.
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
**Type**: Flutter Mobile Application
|
**Type**: Flutter Mobile Application
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import '../models/message.dart';
|
import '../models/message.dart';
|
||||||
import '../models/sar_marker.dart';
|
import '../models/sar_marker.dart';
|
||||||
import '../services/message_storage_service.dart';
|
import '../services/message_storage_service.dart';
|
||||||
|
import '../utils/sar_message_parser.dart';
|
||||||
|
|
||||||
/// Messages Provider - manages message history and SAR markers
|
/// Messages Provider - manages message history and SAR markers
|
||||||
class MessagesProvider with ChangeNotifier {
|
class MessagesProvider with ChangeNotifier {
|
||||||
@@ -45,13 +46,16 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
print('📦 [MessagesProvider] Loading persisted messages...');
|
print('📦 [MessagesProvider] Loading persisted messages...');
|
||||||
final storedMessages = await _storageService.loadMessages();
|
final storedMessages = await _storageService.loadMessages();
|
||||||
|
|
||||||
// Add stored messages
|
// Add stored messages with enhancement to ensure SAR detection
|
||||||
_messages.addAll(storedMessages);
|
|
||||||
|
|
||||||
// Extract SAR markers from stored messages
|
|
||||||
for (final message in storedMessages) {
|
for (final message in storedMessages) {
|
||||||
if (message.isSarMarker) {
|
// Re-enhance each message to ensure SAR markers are properly detected
|
||||||
final marker = message.toSarMarker();
|
// This handles cases where messages were stored before enhancement logic
|
||||||
|
final enhancedMessage = SarMessageParser.enhanceMessage(message);
|
||||||
|
_messages.add(enhancedMessage);
|
||||||
|
|
||||||
|
// Extract SAR markers
|
||||||
|
if (enhancedMessage.isSarMarker) {
|
||||||
|
final marker = enhancedMessage.toSarMarker();
|
||||||
if (marker != null) {
|
if (marker != null) {
|
||||||
_sarMarkers[marker.id] = marker;
|
_sarMarkers[marker.id] = marker;
|
||||||
}
|
}
|
||||||
@@ -69,11 +73,21 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
|
|
||||||
/// Add a message
|
/// Add a message
|
||||||
void addMessage(Message message) {
|
void addMessage(Message message) {
|
||||||
_messages.add(message);
|
// Always enhance message with SAR parser to detect SAR markers
|
||||||
|
final enhancedMessage = SarMessageParser.enhanceMessage(message);
|
||||||
|
|
||||||
|
// Debug: Check if message is SAR
|
||||||
|
if (message.text.startsWith('S:')) {
|
||||||
|
print('🔍 [MessagesProvider] Processing SAR message: ${message.text}');
|
||||||
|
print(' isSarMarker: ${enhancedMessage.isSarMarker}');
|
||||||
|
print(' sarMarkerType: ${enhancedMessage.sarMarkerType}');
|
||||||
|
}
|
||||||
|
|
||||||
|
_messages.add(enhancedMessage);
|
||||||
|
|
||||||
// If it's a SAR marker message, extract and store the marker
|
// If it's a SAR marker message, extract and store the marker
|
||||||
if (message.isSarMarker) {
|
if (enhancedMessage.isSarMarker) {
|
||||||
final marker = message.toSarMarker();
|
final marker = enhancedMessage.toSarMarker();
|
||||||
if (marker != null) {
|
if (marker != null) {
|
||||||
_sarMarkers[marker.id] = marker;
|
_sarMarkers[marker.id] = marker;
|
||||||
}
|
}
|
||||||
@@ -88,10 +102,12 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
/// Add multiple messages
|
/// Add multiple messages
|
||||||
void addMessages(List<Message> messages) {
|
void addMessages(List<Message> messages) {
|
||||||
for (final message in messages) {
|
for (final message in messages) {
|
||||||
_messages.add(message);
|
// Always enhance message with SAR parser to detect SAR markers
|
||||||
|
final enhancedMessage = SarMessageParser.enhanceMessage(message);
|
||||||
|
_messages.add(enhancedMessage);
|
||||||
|
|
||||||
if (message.isSarMarker) {
|
if (enhancedMessage.isSarMarker) {
|
||||||
final marker = message.toSarMarker();
|
final marker = enhancedMessage.toSarMarker();
|
||||||
if (marker != null) {
|
if (marker != null) {
|
||||||
_sarMarkers[marker.id] = marker;
|
_sarMarkers[marker.id] = marker;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -572,23 +572,41 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isSarMarker = message.isSarMarker;
|
final isSarMarker = message.isSarMarker;
|
||||||
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
|
// Debug: Log message details
|
||||||
|
if (message.text.startsWith('S:')) {
|
||||||
|
debugPrint('🎨 [MessageBubble] Rendering SAR message:');
|
||||||
|
debugPrint(' Text: ${message.text}');
|
||||||
|
debugPrint(' isSarMarker: $isSarMarker');
|
||||||
|
debugPrint(' sarMarkerType: ${message.sarMarkerType}');
|
||||||
|
}
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSarMarker
|
color: isSarMarker
|
||||||
? _getSarMarkerColor(context)
|
? _getSarMarkerColor(context, isDarkMode)
|
||||||
: Theme.of(context).colorScheme.surfaceVariant,
|
: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: isSarMarker
|
border: isSarMarker
|
||||||
? Border.all(
|
? Border.all(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: _getSarMarkerBorderColor(context, isDarkMode),
|
||||||
width: 2,
|
width: 3,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
|
boxShadow: isSarMarker
|
||||||
|
? [
|
||||||
|
BoxShadow(
|
||||||
|
color: _getSarMarkerBorderColor(context, isDarkMode).withValues(alpha: 0.3),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -596,46 +614,61 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
// Header: Sender and time
|
// Header: Sender and time
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
if (message.isChannelMessage)
|
|
||||||
const Icon(Icons.tag, size: 16)
|
|
||||||
else
|
|
||||||
const Icon(Icons.person, size: 16),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
message.displaySender,
|
|
||||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
if (isSarMarker)
|
if (isSarMarker)
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 6,
|
horizontal: 10,
|
||||||
vertical: 2,
|
vertical: 4,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: _getSarMarkerBorderColor(context, isDarkMode),
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Row(
|
||||||
'SAR',
|
mainAxisSize: MainAxisSize.min,
|
||||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
children: [
|
||||||
color: Theme.of(context).colorScheme.onPrimary,
|
const Icon(
|
||||||
fontWeight: FontWeight.bold,
|
Icons.warning_amber_rounded,
|
||||||
),
|
size: 16,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'SAR ALERT',
|
||||||
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
else ...[
|
||||||
|
if (message.isChannelMessage)
|
||||||
|
const Icon(Icons.tag, size: 16)
|
||||||
|
else
|
||||||
|
const Icon(Icons.person, size: 16),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
message.displaySender,
|
||||||
|
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
],
|
||||||
|
const Spacer(),
|
||||||
Text(
|
Text(
|
||||||
message.timeAgo,
|
message.timeAgo,
|
||||||
style: Theme.of(context).textTheme.labelSmall,
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
|
fontWeight: isSarMarker ? FontWeight.w600 : FontWeight.normal,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// SAR marker content
|
// SAR marker content (simplified design matching message history)
|
||||||
if (isSarMarker && message.sarMarkerType != null) ...[
|
if (isSarMarker && message.sarMarkerType != null) ...[
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -650,15 +683,16 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
message.sarMarkerType!.displayName,
|
message.sarMarkerType!.displayName,
|
||||||
style:
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
Theme.of(context).textTheme.titleMedium?.copyWith(
|
fontWeight: FontWeight.bold,
|
||||||
fontWeight: FontWeight.bold,
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (message.sarGpsCoordinates != null)
|
if (message.sarGpsCoordinates != null)
|
||||||
Text(
|
Text(
|
||||||
'${message.sarGpsCoordinates!.latitude.toStringAsFixed(5)}, ${message.sarGpsCoordinates!.longitude.toStringAsFixed(5)}',
|
'${message.sarGpsCoordinates!.latitude.toStringAsFixed(5)}, ${message.sarGpsCoordinates!.longitude.toStringAsFixed(5)}',
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -690,8 +724,88 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Color _getSarMarkerColor(BuildContext context) {
|
Color _getSarMarkerColor(BuildContext context, bool isDarkMode) {
|
||||||
return Theme.of(context).colorScheme.primaryContainer;
|
if (message.sarMarkerType == null) {
|
||||||
|
return Theme.of(context).colorScheme.primaryContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use type-specific colors with alpha for background
|
||||||
|
switch (message.sarMarkerType!) {
|
||||||
|
case SarMarkerType.foundPerson:
|
||||||
|
return isDarkMode
|
||||||
|
? const Color(0xFF1B5E20).withValues(alpha: 0.4) // Dark green
|
||||||
|
: const Color(0xFFC8E6C9).withValues(alpha: 0.9); // Light green
|
||||||
|
case SarMarkerType.fire:
|
||||||
|
return isDarkMode
|
||||||
|
? const Color(0xFFB71C1C).withValues(alpha: 0.4) // Dark red
|
||||||
|
: const Color(0xFFFFCDD2).withValues(alpha: 0.9); // Light red
|
||||||
|
case SarMarkerType.stagingArea:
|
||||||
|
return isDarkMode
|
||||||
|
? const Color(0xFF0D47A1).withValues(alpha: 0.4) // Dark blue
|
||||||
|
: const Color(0xFFBBDEFB).withValues(alpha: 0.9); // Light blue
|
||||||
|
case SarMarkerType.object:
|
||||||
|
return isDarkMode
|
||||||
|
? const Color(0xFF4A148C).withValues(alpha: 0.4) // Dark purple
|
||||||
|
: const Color(0xFFE1BEE7).withValues(alpha: 0.9); // Light purple
|
||||||
|
case SarMarkerType.unknown:
|
||||||
|
return isDarkMode
|
||||||
|
? const Color(0xFF424242).withValues(alpha: 0.4) // Dark gray
|
||||||
|
: const Color(0xFFEEEEEE).withValues(alpha: 0.9); // Light gray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getSarMarkerBorderColor(BuildContext context, bool isDarkMode) {
|
||||||
|
if (message.sarMarkerType == null) {
|
||||||
|
return Theme.of(context).colorScheme.primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use vibrant type-specific colors for borders
|
||||||
|
switch (message.sarMarkerType!) {
|
||||||
|
case SarMarkerType.foundPerson:
|
||||||
|
return const Color(0xFF4CAF50); // Green
|
||||||
|
case SarMarkerType.fire:
|
||||||
|
return const Color(0xFFF44336); // Red
|
||||||
|
case SarMarkerType.stagingArea:
|
||||||
|
return const Color(0xFF2196F3); // Blue
|
||||||
|
case SarMarkerType.object:
|
||||||
|
return const Color(0xFF9C27B0); // Purple
|
||||||
|
case SarMarkerType.unknown:
|
||||||
|
return const Color(0xFF9E9E9E); // Gray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract notes from SAR message text
|
||||||
|
/// Returns text after the SAR marker format, or null if none
|
||||||
|
String? _extractNotesFromMessage(String text) {
|
||||||
|
final trimmed = text.trim();
|
||||||
|
if (!trimmed.startsWith('S:')) return null;
|
||||||
|
|
||||||
|
// Extract first line
|
||||||
|
final firstLine = trimmed.split('\n').first;
|
||||||
|
|
||||||
|
// Find the end of coordinates (after second colon and comma-separated numbers)
|
||||||
|
final pattern = RegExp(r'^S:.:(-?\d+\.?\d*),(-?\d+\.?\d*)');
|
||||||
|
final match = pattern.firstMatch(firstLine);
|
||||||
|
if (match == null) return null;
|
||||||
|
|
||||||
|
// Extract notes from same line
|
||||||
|
String? notes;
|
||||||
|
if (match.end < firstLine.length) {
|
||||||
|
notes = firstLine.substring(match.end).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for multi-line notes
|
||||||
|
final lines = trimmed.split('\n');
|
||||||
|
if (lines.length > 1) {
|
||||||
|
final additionalNotes = lines.sublist(1).join('\n').trim();
|
||||||
|
if (additionalNotes.isNotEmpty) {
|
||||||
|
notes = notes != null && notes.isNotEmpty
|
||||||
|
? '$notes\n$additionalNotes'
|
||||||
|
: additionalNotes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return notes != null && notes.isNotEmpty ? notes : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,9 +134,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
foundPersonCount: 2,
|
foundPersonCount: 2,
|
||||||
fireCount: 1,
|
fireCount: 1,
|
||||||
stagingCount: 1,
|
stagingCount: 1,
|
||||||
|
objectCount: 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
final channelMessages = SampleDataGenerator.generateChannelMessages(
|
final channelMessages = SampleDataGenerator.generateChannelMessages(
|
||||||
|
centerLocation: centerLocation,
|
||||||
generalChannelMessages: 8,
|
generalChannelMessages: 8,
|
||||||
emergencyChannelMessages: 5,
|
emergencyChannelMessages: 5,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ class SampleDataGenerator {
|
|||||||
int foundPersonCount = 2,
|
int foundPersonCount = 2,
|
||||||
int fireCount = 1,
|
int fireCount = 1,
|
||||||
int stagingCount = 1,
|
int stagingCount = 1,
|
||||||
|
int objectCount = 1,
|
||||||
}) {
|
}) {
|
||||||
final messages = <Message>[];
|
final messages = <Message>[];
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
@@ -209,14 +210,53 @@ class SampleDataGenerator {
|
|||||||
messageId++;
|
messageId++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate object markers
|
||||||
|
for (int i = 0; i < objectCount; i++) {
|
||||||
|
final latOffset = (_random.nextDouble() - 0.5) * 0.015;
|
||||||
|
final lonOffset = (_random.nextDouble() - 0.5) * 0.015;
|
||||||
|
final lat = centerLocation.latitude + latOffset;
|
||||||
|
final lon = centerLocation.longitude + lonOffset;
|
||||||
|
|
||||||
|
final senderKey = Uint8List.fromList(
|
||||||
|
List.generate(32, (_) => _random.nextInt(256)),
|
||||||
|
);
|
||||||
|
|
||||||
|
final timestamp = now.subtract(Duration(minutes: 40 + i * 5));
|
||||||
|
final notes = [
|
||||||
|
' Backpack found - blue color',
|
||||||
|
' Vehicle abandoned - check for owner',
|
||||||
|
' Camping equipment discovered',
|
||||||
|
' Trail marker found off-path',
|
||||||
|
];
|
||||||
|
|
||||||
|
messages.add(Message(
|
||||||
|
id: 'sample_object_$messageId',
|
||||||
|
messageType: MessageType.contact,
|
||||||
|
senderPublicKeyPrefix: senderKey.sublist(0, 6),
|
||||||
|
pathLen: 1,
|
||||||
|
textType: MessageTextType.plain,
|
||||||
|
senderTimestamp: timestamp.millisecondsSinceEpoch ~/ 1000,
|
||||||
|
text: 'S:📦:${lat.toStringAsFixed(4)},${lon.toStringAsFixed(4)}${notes[i % notes.length]}',
|
||||||
|
receivedAt: timestamp,
|
||||||
|
isSarMarker: true,
|
||||||
|
sarMarkerType: SarMarkerType.object,
|
||||||
|
sarGpsCoordinates: LatLng(lat, lon),
|
||||||
|
senderName: 'Sample Searcher',
|
||||||
|
));
|
||||||
|
messageId++;
|
||||||
|
}
|
||||||
|
|
||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate sample channel messages for public channels
|
/// Generate sample channel messages for public channels
|
||||||
static List<Message> generateChannelMessages({
|
static List<Message> generateChannelMessages({
|
||||||
|
LatLng? centerLocation,
|
||||||
int generalChannelMessages = 8,
|
int generalChannelMessages = 8,
|
||||||
int emergencyChannelMessages = 5,
|
int emergencyChannelMessages = 5,
|
||||||
}) {
|
}) {
|
||||||
|
// Use provided location or default to Ljubljana, Slovenia
|
||||||
|
final center = centerLocation ?? const LatLng(46.0569, 14.5058);
|
||||||
final messages = <Message>[];
|
final messages = <Message>[];
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
int messageId = 1000; // Start with high ID to avoid conflicts
|
int messageId = 1000; // Start with high ID to avoid conflicts
|
||||||
@@ -238,16 +278,17 @@ class SampleDataGenerator {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Sample messages for Emergency channel (index 1)
|
// Sample messages for Emergency channel (index 1)
|
||||||
|
// Mix regular messages and SAR markers
|
||||||
final emergencyMessages = [
|
final emergencyMessages = [
|
||||||
'URGENT: Medical assistance needed at sector 4',
|
'URGENT: Medical assistance needed at sector 4',
|
||||||
'Found person - requesting immediate evac',
|
'S:🧑:${center.latitude.toStringAsFixed(4)},${(center.longitude + 0.005).toStringAsFixed(4)} Adult male, conscious',
|
||||||
'Fire spotted - coordinates incoming',
|
'Fire spotted - coordinates incoming',
|
||||||
'Team member injured - sending location',
|
'S:🔥:${(center.latitude + 0.008).toStringAsFixed(4)},${(center.longitude + 0.003).toStringAsFixed(4)} Spreading rapidly!',
|
||||||
'PRIORITY: Need helicopter support',
|
'PRIORITY: Need helicopter support',
|
||||||
'Medical team en route to your location',
|
'Medical team en route to your location',
|
||||||
'Evac helicopter ETA 10 minutes',
|
'Evac helicopter ETA 10 minutes',
|
||||||
'Emergency resolved - all clear',
|
'Emergency resolved - all clear',
|
||||||
'Casualties: 1 minor injury, being treated',
|
'S:🏕️:${(center.latitude - 0.002).toStringAsFixed(4)},${(center.longitude - 0.004).toStringAsFixed(4)} Emergency staging area',
|
||||||
'Emergency services notified and responding',
|
'Emergency services notified and responding',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -9,14 +9,19 @@ import '../models/message.dart';
|
|||||||
/// S:🔥:40.7128,-74.0060
|
/// S:🔥:40.7128,-74.0060
|
||||||
/// S:🏕️:34.0522,-118.2437
|
/// S:🏕️:34.0522,-118.2437
|
||||||
class SarMessageParser {
|
class SarMessageParser {
|
||||||
|
// Updated regex to allow optional notes after coordinates
|
||||||
|
// Captures: emoji (one or more non-colon chars), latitude, longitude
|
||||||
|
// Note: Emojis are multi-byte characters, so we use [^:]+ instead of .
|
||||||
static final RegExp _sarPattern = RegExp(
|
static final RegExp _sarPattern = RegExp(
|
||||||
r'^S:(.):(-?\d+\.?\d*),(-?\d+\.?\d*)$',
|
r'^S:([^:]+):(-?\d+\.?\d*),(-?\d+\.?\d*)',
|
||||||
multiLine: false,
|
multiLine: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Check if a message is a SAR marker message
|
/// Check if a message is a SAR marker message
|
||||||
static bool isSarMessage(String text) {
|
static bool isSarMessage(String text) {
|
||||||
return text.trim().startsWith('S:') && _sarPattern.hasMatch(text.trim());
|
// Extract just the first line for matching
|
||||||
|
final firstLine = text.trim().split('\n').first;
|
||||||
|
return firstLine.startsWith('S:') && _sarPattern.hasMatch(firstLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a SAR message and extract marker information
|
/// Parse a SAR message and extract marker information
|
||||||
@@ -25,7 +30,9 @@ class SarMessageParser {
|
|||||||
final trimmed = text.trim();
|
final trimmed = text.trim();
|
||||||
if (!trimmed.startsWith('S:')) return null;
|
if (!trimmed.startsWith('S:')) return null;
|
||||||
|
|
||||||
final match = _sarPattern.firstMatch(trimmed);
|
// Extract first line (actual SAR marker)
|
||||||
|
final firstLine = trimmed.split('\n').first;
|
||||||
|
final match = _sarPattern.firstMatch(firstLine);
|
||||||
if (match == null) return null;
|
if (match == null) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -40,10 +47,24 @@ class SarMessageParser {
|
|||||||
final markerType = SarMarkerType.fromEmoji(emoji);
|
final markerType = SarMarkerType.fromEmoji(emoji);
|
||||||
final location = LatLng(latitude, longitude);
|
final location = LatLng(latitude, longitude);
|
||||||
|
|
||||||
|
// Extract notes if present (everything after coordinates on first line, or subsequent lines)
|
||||||
|
String? notes;
|
||||||
|
final coordsEnd = match.end;
|
||||||
|
if (coordsEnd < firstLine.length) {
|
||||||
|
// Notes on same line after coordinates
|
||||||
|
notes = firstLine.substring(coordsEnd).trim();
|
||||||
|
}
|
||||||
|
// Check for multi-line notes
|
||||||
|
final additionalNotes = extractNotes(text);
|
||||||
|
if (additionalNotes != null) {
|
||||||
|
notes = notes != null ? '$notes\n$additionalNotes' : additionalNotes;
|
||||||
|
}
|
||||||
|
|
||||||
return SarMarkerInfo(
|
return SarMarkerInfo(
|
||||||
type: markerType,
|
type: markerType,
|
||||||
location: location,
|
location: location,
|
||||||
emoji: emoji,
|
emoji: emoji,
|
||||||
|
notes: notes,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
@@ -139,15 +160,17 @@ class SarMarkerInfo {
|
|||||||
final SarMarkerType type;
|
final SarMarkerType type;
|
||||||
final LatLng location;
|
final LatLng location;
|
||||||
final String emoji;
|
final String emoji;
|
||||||
|
final String? notes;
|
||||||
|
|
||||||
SarMarkerInfo({
|
SarMarkerInfo({
|
||||||
required this.type,
|
required this.type,
|
||||||
required this.location,
|
required this.location,
|
||||||
required this.emoji,
|
required this.emoji,
|
||||||
|
this.notes,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SarMarkerInfo(type: ${type.displayName}, location: $location)';
|
return 'SarMarkerInfo(type: ${type.displayName}, location: $location, notes: $notes)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user