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:
Janez T
2025-10-23 19:17:13 +02:00
parent b70eed1bf2
commit 98cda8c976
28 changed files with 2734 additions and 270 deletions

View File

@@ -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

View File

@@ -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)';
}
}