mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-12 00:40:28 +00:00
feat: Enhance SAR message handling with color indexing and localization
- Added `sarColorIndex` to `Message` and `SarMarker` models to support color indexing for SAR markers. - Introduced a standard color palette in `SarTemplate` and methods to retrieve colors based on index. - Updated `SarTemplate` to include localization for template names. - Modified `SarUpdateSheet` and message sending logic to include color index in SAR messages. - Enhanced `SarMessageParser` to support both old and new message formats with color index. - Updated UI components to reflect changes in SAR marker color handling based on the new color index. - Improved sample data generation to utilize localized strings for better testing and demonstration.
This commit is contained in:
@@ -5,6 +5,7 @@ import '../models/contact.dart';
|
||||
import '../models/contact_telemetry.dart';
|
||||
import '../models/message.dart';
|
||||
import '../models/sar_marker.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
/// Generates sample data for testing/demo purposes
|
||||
class SampleDataGenerator {
|
||||
@@ -13,6 +14,7 @@ class SampleDataGenerator {
|
||||
/// Generate sample contacts around a center location
|
||||
static List<Contact> generateContacts({
|
||||
required LatLng centerLocation,
|
||||
required AppLocalizations l10n,
|
||||
int teamMemberCount = 5,
|
||||
int channelCount = 2,
|
||||
}) {
|
||||
@@ -20,21 +22,21 @@ class SampleDataGenerator {
|
||||
final now = DateTime.now();
|
||||
|
||||
final teamNames = [
|
||||
'👮Police Lead',
|
||||
'🚁Drone Operator',
|
||||
'🧑🏻🚒Firefighter Alpha',
|
||||
'🧑⚕️Medic Charlie',
|
||||
'📡Command Delta',
|
||||
'🚒Fire Engine',
|
||||
'👨✈️Air Support',
|
||||
'🧑💼Base Coordinator',
|
||||
'👮${l10n.samplePoliceLead}',
|
||||
'🚁${l10n.sampleDroneOperator}',
|
||||
'🧑🏻🚒${l10n.sampleFirefighterAlpha}',
|
||||
'🧑⚕️${l10n.sampleMedicCharlie}',
|
||||
'📡${l10n.sampleCommandDelta}',
|
||||
'🚒${l10n.sampleFireEngine}',
|
||||
'👨✈️${l10n.sampleAirSupport}',
|
||||
'🧑💼${l10n.sampleBaseCoordinator}',
|
||||
];
|
||||
|
||||
final channelNames = [
|
||||
'General',
|
||||
'Emergency',
|
||||
'Coordination',
|
||||
'Updates',
|
||||
l10n.general,
|
||||
l10n.channelEmergency,
|
||||
l10n.channelCoordination,
|
||||
l10n.channelUpdates,
|
||||
];
|
||||
|
||||
// Generate team members (chat contacts)
|
||||
@@ -114,6 +116,7 @@ class SampleDataGenerator {
|
||||
/// Generate sample SAR markers around a center location
|
||||
static List<Message> generateSarMarkerMessages({
|
||||
required LatLng centerLocation,
|
||||
required AppLocalizations l10n,
|
||||
int foundPersonCount = 2,
|
||||
int fireCount = 1,
|
||||
int stagingCount = 1,
|
||||
@@ -147,7 +150,7 @@ class SampleDataGenerator {
|
||||
isSarMarker: true,
|
||||
sarMarkerType: SarMarkerType.foundPerson,
|
||||
sarGpsCoordinates: LatLng(lat, lon),
|
||||
senderName: 'Sample Team Member',
|
||||
senderName: l10n.sampleTeamMember,
|
||||
));
|
||||
messageId++;
|
||||
}
|
||||
@@ -176,7 +179,7 @@ class SampleDataGenerator {
|
||||
isSarMarker: true,
|
||||
sarMarkerType: SarMarkerType.fire,
|
||||
sarGpsCoordinates: LatLng(lat, lon),
|
||||
senderName: 'Sample Scout',
|
||||
senderName: l10n.sampleScout,
|
||||
));
|
||||
messageId++;
|
||||
}
|
||||
@@ -205,7 +208,7 @@ class SampleDataGenerator {
|
||||
isSarMarker: true,
|
||||
sarMarkerType: SarMarkerType.stagingArea,
|
||||
sarGpsCoordinates: LatLng(lat, lon),
|
||||
senderName: 'Sample Base',
|
||||
senderName: l10n.sampleBase,
|
||||
));
|
||||
messageId++;
|
||||
}
|
||||
@@ -223,10 +226,10 @@ class SampleDataGenerator {
|
||||
|
||||
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',
|
||||
l10n.sampleObjectBackpack,
|
||||
l10n.sampleObjectVehicle,
|
||||
l10n.sampleObjectCamping,
|
||||
l10n.sampleObjectTrailMarker,
|
||||
];
|
||||
|
||||
messages.add(Message(
|
||||
@@ -241,7 +244,7 @@ class SampleDataGenerator {
|
||||
isSarMarker: true,
|
||||
sarMarkerType: SarMarkerType.object,
|
||||
sarGpsCoordinates: LatLng(lat, lon),
|
||||
senderName: 'Sample Searcher',
|
||||
senderName: l10n.sampleSearcher,
|
||||
));
|
||||
messageId++;
|
||||
}
|
||||
@@ -252,6 +255,7 @@ class SampleDataGenerator {
|
||||
/// Generate sample channel messages for public channels
|
||||
static List<Message> generateChannelMessages({
|
||||
LatLng? centerLocation,
|
||||
required AppLocalizations l10n,
|
||||
int generalChannelMessages = 8,
|
||||
int emergencyChannelMessages = 5,
|
||||
}) {
|
||||
@@ -263,44 +267,44 @@ class SampleDataGenerator {
|
||||
|
||||
// Sample messages for General channel (index 0)
|
||||
final generalMessages = [
|
||||
'All teams check in',
|
||||
'Weather update: Clear skies, temp 18°C',
|
||||
'Base camp established at staging area',
|
||||
'Team Alpha moving to sector 2',
|
||||
'Radio check - all stations respond',
|
||||
'Water supply available at checkpoint 3',
|
||||
'Team Bravo reporting: sector 1 clear',
|
||||
'ETA to rally point: 15 minutes',
|
||||
'Supply drop confirmed for 14:00',
|
||||
'Drone survey completed - no findings',
|
||||
'Team Charlie requesting backup',
|
||||
'All units: maintain radio discipline',
|
||||
l10n.sampleMsgAllTeamsCheckIn,
|
||||
l10n.sampleMsgWeatherUpdate,
|
||||
l10n.sampleMsgBaseCamp,
|
||||
l10n.sampleMsgTeamAlpha,
|
||||
l10n.sampleMsgRadioCheck,
|
||||
l10n.sampleMsgWaterSupply,
|
||||
l10n.sampleMsgTeamBravo,
|
||||
l10n.sampleMsgEtaRallyPoint,
|
||||
l10n.sampleMsgSupplyDrop,
|
||||
l10n.sampleMsgDroneSurvey,
|
||||
l10n.sampleMsgTeamCharlie,
|
||||
l10n.sampleMsgRadioDiscipline,
|
||||
];
|
||||
|
||||
// Sample messages for Emergency channel (index 1)
|
||||
// Mix regular messages and SAR markers
|
||||
final emergencyMessages = [
|
||||
'URGENT: Medical assistance needed at sector 4',
|
||||
'S:🧑:${center.latitude.toStringAsFixed(5)},${(center.longitude + 0.005).toStringAsFixed(5)} Adult male, conscious',
|
||||
'Fire spotted - coordinates incoming',
|
||||
'S:🔥:${(center.latitude + 0.008).toStringAsFixed(5)},${(center.longitude + 0.003).toStringAsFixed(5)} Spreading rapidly!',
|
||||
'PRIORITY: Need helicopter support',
|
||||
'Medical team en route to your location',
|
||||
'Evac helicopter ETA 10 minutes',
|
||||
'Emergency resolved - all clear',
|
||||
'S:🏕️:${(center.latitude - 0.002).toStringAsFixed(5)},${(center.longitude - 0.004).toStringAsFixed(5)} Emergency staging area',
|
||||
'Emergency services notified and responding',
|
||||
l10n.sampleMsgUrgentMedical,
|
||||
'S:🧑:${center.latitude.toStringAsFixed(5)},${(center.longitude + 0.005).toStringAsFixed(5)}${l10n.sampleMsgAdultMale}',
|
||||
l10n.sampleMsgFireSpotted,
|
||||
'S:🔥:${(center.latitude + 0.008).toStringAsFixed(5)},${(center.longitude + 0.003).toStringAsFixed(5)}${l10n.sampleMsgSpreadingRapidly}',
|
||||
l10n.sampleMsgPriorityHelicopter,
|
||||
l10n.sampleMsgMedicalTeamEnRoute,
|
||||
l10n.sampleMsgEvacHelicopter,
|
||||
l10n.sampleMsgEmergencyResolved,
|
||||
'S:🏕️:${(center.latitude - 0.002).toStringAsFixed(5)},${(center.longitude - 0.004).toStringAsFixed(5)}${l10n.sampleMsgEmergencyStagingArea}',
|
||||
l10n.sampleMsgEmergencyServices,
|
||||
];
|
||||
|
||||
final teamNames = [
|
||||
'Alpha Team Lead',
|
||||
'Bravo Scout',
|
||||
'Charlie Medic',
|
||||
'Delta Navigator',
|
||||
'Echo Support',
|
||||
'Base Command',
|
||||
'Field Coordinator',
|
||||
'Medical Team',
|
||||
l10n.sampleAlphaTeamLead,
|
||||
l10n.sampleBravoScout,
|
||||
l10n.sampleCharlieMedic,
|
||||
l10n.sampleDeltaNavigator,
|
||||
l10n.sampleEchoSupport,
|
||||
l10n.sampleBaseCommand,
|
||||
l10n.sampleFieldCoordinator,
|
||||
l10n.sampleMedicalTeam,
|
||||
];
|
||||
|
||||
// Generate General channel messages
|
||||
|
||||
@@ -3,16 +3,23 @@ import '../models/sar_marker.dart';
|
||||
import '../models/message.dart';
|
||||
|
||||
/// Parser for SAR (Search & Rescue) special messages
|
||||
/// Format: S:<emoji>:<latitude>,<longitude>:<optional_message>
|
||||
/// Old format: S:<emoji>:<latitude>,<longitude>:<optional_message>
|
||||
/// New format: S:<emoji>:<colorIndex>:<latitude>,<longitude>:<optional_message>
|
||||
/// Examples:
|
||||
/// S:🧑:37.7749,-122.4194
|
||||
/// S:🔥:40.7128,-74.0060:Large wildfire spreading
|
||||
/// S:🏕️:34.0522,-118.2437:Base camp established
|
||||
/// S:🧑:37.7749,-122.4194 (old format)
|
||||
/// S:🧑:2:37.7749,-122.4194 (new format with green color)
|
||||
/// S:🔥:0:40.7128,-74.0060:Large wildfire spreading (new format with red color)
|
||||
class SarMessageParser {
|
||||
// Updated regex to capture optional message after coordinates
|
||||
// Captures: emoji (one or more non-colon chars), latitude, longitude, optional message
|
||||
// Note: Emojis are multi-byte characters, so we use [^:]+ instead of .
|
||||
static final RegExp _sarPattern = RegExp(
|
||||
// Regex for new format with color index: S:emoji:colorIndex:lat,lon:notes
|
||||
// Captures: emoji, colorIndex (single digit), latitude, longitude, optional message
|
||||
static final RegExp _sarPatternNew = RegExp(
|
||||
r'^S:([^:]+):(\d):(-?\d+\.?\d*),(-?\d+\.?\d*):?(.*)',
|
||||
multiLine: false,
|
||||
);
|
||||
|
||||
// Regex for old format (backward compatibility): S:emoji:lat,lon:notes
|
||||
// Captures: emoji, latitude, longitude, optional message
|
||||
static final RegExp _sarPatternOld = RegExp(
|
||||
r'^S:([^:]+):(-?\d+\.?\d*),(-?\d+\.?\d*):?(.*)',
|
||||
multiLine: false,
|
||||
);
|
||||
@@ -21,30 +28,61 @@ class SarMessageParser {
|
||||
static bool isSarMessage(String text) {
|
||||
// Extract just the first line for matching
|
||||
final firstLine = text.trim().split('\n').first;
|
||||
return firstLine.startsWith('S:') && _sarPattern.hasMatch(firstLine);
|
||||
return firstLine.startsWith('S:') && (_sarPatternNew.hasMatch(firstLine) || _sarPatternOld.hasMatch(firstLine));
|
||||
}
|
||||
|
||||
/// Parse a SAR message and extract marker information
|
||||
/// Returns null if the message is not a valid SAR message
|
||||
/// Supports both old format (S:emoji:lat,lon:notes) and new format (S:emoji:colorIndex:lat,lon:notes)
|
||||
static SarMarkerInfo? parse(String text) {
|
||||
final trimmed = text.trim();
|
||||
if (!trimmed.startsWith('S:')) return null;
|
||||
|
||||
// Extract first line (actual SAR marker)
|
||||
final firstLine = trimmed.split('\n').first;
|
||||
final match = _sarPattern.firstMatch(firstLine);
|
||||
if (match == null) return null;
|
||||
|
||||
// Try new format first (with color index)
|
||||
var match = _sarPatternNew.firstMatch(firstLine);
|
||||
bool isNewFormat = match != null;
|
||||
|
||||
// If new format didn't match, try old format
|
||||
if (match == null) {
|
||||
match = _sarPatternOld.firstMatch(firstLine);
|
||||
if (match == null) return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final emoji = match.group(1)!;
|
||||
final latitude = double.parse(match.group(2)!);
|
||||
final longitude = double.parse(match.group(3)!);
|
||||
final inlineMessage = match.group(4)?.trim(); // Optional message after colon
|
||||
String emoji;
|
||||
double latitude;
|
||||
double longitude;
|
||||
String? inlineMessage;
|
||||
int? colorIndex;
|
||||
|
||||
if (isNewFormat) {
|
||||
// New format: S:emoji:colorIndex:lat,lon:notes
|
||||
emoji = match!.group(1)!;
|
||||
colorIndex = int.parse(match.group(2)!);
|
||||
latitude = double.parse(match.group(3)!);
|
||||
longitude = double.parse(match.group(4)!);
|
||||
inlineMessage = match.group(5)?.trim();
|
||||
} else {
|
||||
// Old format: S:emoji:lat,lon:notes
|
||||
emoji = match!.group(1)!;
|
||||
colorIndex = null; // No color index in old format
|
||||
latitude = double.parse(match.group(2)!);
|
||||
longitude = double.parse(match.group(3)!);
|
||||
inlineMessage = match.group(4)?.trim();
|
||||
}
|
||||
|
||||
// Validate coordinates
|
||||
if (latitude < -90 || latitude > 90) return null;
|
||||
if (longitude < -180 || longitude > 180) return null;
|
||||
|
||||
// Validate color index if present
|
||||
if (colorIndex != null && (colorIndex < 0 || colorIndex > 7)) {
|
||||
colorIndex = null; // Invalid index, ignore it
|
||||
}
|
||||
|
||||
final markerType = SarMarkerType.fromEmoji(emoji);
|
||||
final location = LatLng(latitude, longitude);
|
||||
|
||||
@@ -65,6 +103,7 @@ class SarMessageParser {
|
||||
location: location,
|
||||
emoji: emoji,
|
||||
notes: notes,
|
||||
colorIndex: colorIndex,
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
@@ -84,16 +123,20 @@ class SarMessageParser {
|
||||
sarCustomEmoji: sarInfo.type == SarMarkerType.unknown
|
||||
? sarInfo.emoji // Preserve custom emoji for unknown types
|
||||
: null,
|
||||
sarColorIndex: sarInfo.colorIndex, // Store color index
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a SAR marker message text
|
||||
/// Create a SAR marker message text (new format with color index)
|
||||
static String createSarMessage({
|
||||
required SarMarkerType type,
|
||||
required LatLng location,
|
||||
String? notes,
|
||||
int? colorIndex,
|
||||
}) {
|
||||
final text = 'S:${type.emoji}:${location.latitude},${location.longitude}';
|
||||
// New format: S:emoji:colorIndex:lat,lon:notes
|
||||
final colorIdx = colorIndex ?? 0; // Default to red if not specified
|
||||
final text = 'S:${type.emoji}:$colorIdx:${location.latitude},${location.longitude}';
|
||||
if (notes != null && notes.isNotEmpty) {
|
||||
// Use colon-separated format for inline message
|
||||
return '$text:$notes';
|
||||
@@ -166,16 +209,18 @@ class SarMarkerInfo {
|
||||
final LatLng location;
|
||||
final String emoji;
|
||||
final String? notes;
|
||||
final int? colorIndex; // Color index from standard palette (0-7), null for backward compatibility
|
||||
|
||||
SarMarkerInfo({
|
||||
required this.type,
|
||||
required this.location,
|
||||
required this.emoji,
|
||||
this.notes,
|
||||
this.colorIndex,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SarMarkerInfo(type: ${type.displayName}, location: $location, notes: $notes)';
|
||||
return 'SarMarkerInfo(type: ${type.displayName}, location: $location, colorIndex: $colorIndex, notes: $notes)';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user