745 lines
23 KiB
Dart
745 lines
23 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
|
|
/// Type of group ride location / stop.
|
|
enum GroupRideLocationType {
|
|
meetup,
|
|
fuel,
|
|
rest,
|
|
food,
|
|
scenic,
|
|
waypoint,
|
|
destination,
|
|
other;
|
|
|
|
String get label {
|
|
switch (this) {
|
|
case GroupRideLocationType.meetup:
|
|
return 'Meeting Point';
|
|
case GroupRideLocationType.fuel:
|
|
return 'Fuel Stop';
|
|
case GroupRideLocationType.rest:
|
|
return 'Rest Stop';
|
|
case GroupRideLocationType.food:
|
|
return 'Food & Drink';
|
|
case GroupRideLocationType.scenic:
|
|
return 'Scenic Overlook';
|
|
case GroupRideLocationType.waypoint:
|
|
return 'Waypoint';
|
|
case GroupRideLocationType.destination:
|
|
return 'Destination';
|
|
case GroupRideLocationType.other:
|
|
return 'Stop';
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Explicit shared coordinate and description for group meetups, stops, and destinations.
|
|
/// Never derived from or tied to private home addresses or live GPS.
|
|
@immutable
|
|
class GroupRideLocation {
|
|
final double latitude;
|
|
final double longitude;
|
|
final String displayName;
|
|
final String address;
|
|
final GroupRideLocationType locationType;
|
|
final String notes;
|
|
final int? stopDurationMinutes;
|
|
|
|
const GroupRideLocation({
|
|
required this.latitude,
|
|
required this.longitude,
|
|
required this.displayName,
|
|
this.address = '',
|
|
this.locationType = GroupRideLocationType.waypoint,
|
|
this.notes = '',
|
|
this.stopDurationMinutes,
|
|
});
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'latitude': latitude,
|
|
'longitude': longitude,
|
|
'displayName': displayName,
|
|
'address': address,
|
|
'locationType': locationType.name,
|
|
'notes': notes,
|
|
if (stopDurationMinutes != null)
|
|
'stopDurationMinutes': stopDurationMinutes,
|
|
};
|
|
|
|
factory GroupRideLocation.fromJson(Map<String, dynamic> json) {
|
|
return GroupRideLocation(
|
|
latitude: (json['latitude'] as num?)?.toDouble() ?? 0.0,
|
|
longitude: (json['longitude'] as num?)?.toDouble() ?? 0.0,
|
|
displayName: json['displayName'] as String? ?? '',
|
|
address: json['address'] as String? ?? '',
|
|
locationType: GroupRideLocationType.values.firstWhere(
|
|
(t) => t.name == (json['locationType'] as String?),
|
|
orElse: () => GroupRideLocationType.waypoint,
|
|
),
|
|
notes: json['notes'] as String? ?? '',
|
|
stopDurationMinutes: json['stopDurationMinutes'] as int?,
|
|
);
|
|
}
|
|
|
|
GroupRideLocation copyWith({
|
|
double? latitude,
|
|
double? longitude,
|
|
String? displayName,
|
|
String? address,
|
|
GroupRideLocationType? locationType,
|
|
String? notes,
|
|
int? stopDurationMinutes,
|
|
}) {
|
|
return GroupRideLocation(
|
|
latitude: latitude ?? this.latitude,
|
|
longitude: longitude ?? this.longitude,
|
|
displayName: displayName ?? this.displayName,
|
|
address: address ?? this.address,
|
|
locationType: locationType ?? this.locationType,
|
|
notes: notes ?? this.notes,
|
|
stopDurationMinutes: stopDurationMinutes ?? this.stopDurationMinutes,
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is GroupRideLocation &&
|
|
runtimeType == other.runtimeType &&
|
|
latitude == other.latitude &&
|
|
longitude == other.longitude &&
|
|
displayName == other.displayName &&
|
|
address == other.address &&
|
|
locationType == other.locationType &&
|
|
notes == other.notes &&
|
|
stopDurationMinutes == other.stopDurationMinutes;
|
|
|
|
@override
|
|
int get hashCode =>
|
|
latitude.hashCode ^
|
|
longitude.hashCode ^
|
|
displayName.hashCode ^
|
|
address.hashCode ^
|
|
locationType.hashCode ^
|
|
notes.hashCode ^
|
|
stopDurationMinutes.hashCode;
|
|
}
|
|
|
|
/// Visibility control for discovering and viewing a Group Ride.
|
|
enum GroupRideVisibility {
|
|
private,
|
|
friendsOnly,
|
|
public;
|
|
|
|
String get label {
|
|
switch (this) {
|
|
case GroupRideVisibility.private:
|
|
return 'Private (Invite Only)';
|
|
case GroupRideVisibility.friendsOnly:
|
|
return 'Friends Only';
|
|
case GroupRideVisibility.public:
|
|
return 'Public (Discoverable)';
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Policy for joining a Group Ride.
|
|
enum GroupRideJoinPolicy {
|
|
inviteOnly,
|
|
requestToJoin,
|
|
open;
|
|
|
|
String get label {
|
|
switch (this) {
|
|
case GroupRideJoinPolicy.inviteOnly:
|
|
return 'Invite Only';
|
|
case GroupRideJoinPolicy.requestToJoin:
|
|
return 'Request to Join';
|
|
case GroupRideJoinPolicy.open:
|
|
return 'Open to Join';
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Status lifecycle of a Group Ride.
|
|
enum GroupRideStatus {
|
|
scheduled,
|
|
assembling,
|
|
underway,
|
|
completed,
|
|
canceled;
|
|
|
|
String get label {
|
|
switch (this) {
|
|
case GroupRideStatus.scheduled:
|
|
return 'Scheduled';
|
|
case GroupRideStatus.assembling:
|
|
return 'Assembling';
|
|
case GroupRideStatus.underway:
|
|
return 'Underway';
|
|
case GroupRideStatus.completed:
|
|
return 'Completed';
|
|
case GroupRideStatus.canceled:
|
|
return 'Canceled';
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Role of a participant in a Group Ride.
|
|
enum GroupRideRole {
|
|
organizer,
|
|
participant,
|
|
lead,
|
|
sweep;
|
|
|
|
String get label {
|
|
switch (this) {
|
|
case GroupRideRole.organizer:
|
|
return 'Organizer';
|
|
case GroupRideRole.participant:
|
|
return 'Rider';
|
|
case GroupRideRole.lead:
|
|
return 'Ride Lead';
|
|
case GroupRideRole.sweep:
|
|
return 'Sweep';
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Status of an individual participant in a Group Ride.
|
|
enum ParticipantStatus {
|
|
invited,
|
|
requested,
|
|
accepted,
|
|
declined,
|
|
left,
|
|
removed;
|
|
|
|
bool get isActiveMember => this == ParticipantStatus.accepted;
|
|
}
|
|
|
|
/// Individual participant membership in a Group Ride.
|
|
@immutable
|
|
class GroupRideParticipant {
|
|
final String userId;
|
|
final GroupRideRole role;
|
|
final ParticipantStatus status;
|
|
final String riderHandle;
|
|
final String riderDisplayName;
|
|
final String? riderAvatarPreset;
|
|
final String? riderAvatarUrl;
|
|
final String? invitedBy;
|
|
final DateTime createdAt;
|
|
final DateTime updatedAt;
|
|
|
|
const GroupRideParticipant({
|
|
required this.userId,
|
|
required this.role,
|
|
required this.status,
|
|
required this.riderHandle,
|
|
required this.riderDisplayName,
|
|
this.riderAvatarPreset,
|
|
this.riderAvatarUrl,
|
|
this.invitedBy,
|
|
required this.createdAt,
|
|
required this.updatedAt,
|
|
});
|
|
|
|
String get displayHandle => '@$riderHandle';
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'userId': userId,
|
|
'role': role.name,
|
|
'status': status.name,
|
|
'riderHandle': riderHandle,
|
|
'riderDisplayName': riderDisplayName,
|
|
if (riderAvatarPreset != null) 'riderAvatarPreset': riderAvatarPreset,
|
|
if (riderAvatarUrl != null) 'riderAvatarUrl': riderAvatarUrl,
|
|
if (invitedBy != null) 'invitedBy': invitedBy,
|
|
'createdAt': createdAt.toIso8601String(),
|
|
'updatedAt': updatedAt.toIso8601String(),
|
|
};
|
|
|
|
factory GroupRideParticipant.fromJson(Map<String, dynamic> json) {
|
|
final created = json['createdAt'] != null
|
|
? DateTime.tryParse(json['createdAt'] as String) ?? DateTime.now()
|
|
: DateTime.now();
|
|
final updated = json['updatedAt'] != null
|
|
? DateTime.tryParse(json['updatedAt'] as String) ?? created
|
|
: created;
|
|
|
|
return GroupRideParticipant(
|
|
userId: json['userId'] as String? ?? '',
|
|
role: GroupRideRole.values.firstWhere(
|
|
(r) => r.name == (json['role'] as String?),
|
|
orElse: () => GroupRideRole.participant,
|
|
),
|
|
status: ParticipantStatus.values.firstWhere(
|
|
(s) => s.name == (json['status'] as String?),
|
|
orElse: () => ParticipantStatus.invited,
|
|
),
|
|
riderHandle: json['riderHandle'] as String? ?? '',
|
|
riderDisplayName: json['riderDisplayName'] as String? ?? '',
|
|
riderAvatarPreset: json['riderAvatarPreset'] as String?,
|
|
riderAvatarUrl: json['riderAvatarUrl'] as String?,
|
|
invitedBy: json['invitedBy'] as String?,
|
|
createdAt: created,
|
|
updatedAt: updated,
|
|
);
|
|
}
|
|
|
|
GroupRideParticipant copyWith({
|
|
String? userId,
|
|
GroupRideRole? role,
|
|
ParticipantStatus? status,
|
|
String? riderHandle,
|
|
String? riderDisplayName,
|
|
String? riderAvatarPreset,
|
|
String? riderAvatarUrl,
|
|
String? invitedBy,
|
|
DateTime? createdAt,
|
|
DateTime? updatedAt,
|
|
}) {
|
|
return GroupRideParticipant(
|
|
userId: userId ?? this.userId,
|
|
role: role ?? this.role,
|
|
status: status ?? this.status,
|
|
riderHandle: riderHandle ?? this.riderHandle,
|
|
riderDisplayName: riderDisplayName ?? this.riderDisplayName,
|
|
riderAvatarPreset: riderAvatarPreset ?? this.riderAvatarPreset,
|
|
riderAvatarUrl: riderAvatarUrl ?? this.riderAvatarUrl,
|
|
invitedBy: invitedBy ?? this.invitedBy,
|
|
createdAt: createdAt ?? this.createdAt,
|
|
updatedAt: updatedAt ?? this.updatedAt,
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is GroupRideParticipant &&
|
|
runtimeType == other.runtimeType &&
|
|
userId == other.userId &&
|
|
role == other.role &&
|
|
status == other.status &&
|
|
riderHandle == other.riderHandle &&
|
|
riderDisplayName == other.riderDisplayName &&
|
|
riderAvatarPreset == other.riderAvatarPreset &&
|
|
riderAvatarUrl == other.riderAvatarUrl &&
|
|
invitedBy == other.invitedBy;
|
|
|
|
@override
|
|
int get hashCode =>
|
|
userId.hashCode ^
|
|
role.hashCode ^
|
|
status.hashCode ^
|
|
riderHandle.hashCode ^
|
|
riderDisplayName.hashCode ^
|
|
riderAvatarPreset.hashCode ^
|
|
riderAvatarUrl.hashCode ^
|
|
invitedBy.hashCode;
|
|
}
|
|
|
|
/// Core Group Ride model.
|
|
@immutable
|
|
class GroupRide {
|
|
final String id;
|
|
final String organizerUid;
|
|
final String organizerHandle;
|
|
final String organizerDisplayName;
|
|
final String title;
|
|
final String description;
|
|
final GroupRideVisibility visibility;
|
|
final GroupRideJoinPolicy joinPolicy;
|
|
final GroupRideStatus status;
|
|
final DateTime scheduledStartAt; // Meetup / arrival time
|
|
final DateTime departureAt; // Kickstands up (>= scheduledStartAt)
|
|
final int? participantLimit;
|
|
final int participantCount; // Authoritative active member counter
|
|
final GroupRideLocation meetingPoint;
|
|
final GroupRideLocation? destination;
|
|
final List<GroupRideLocation> stops; // Ordered waypoints
|
|
final DateTime createdAt;
|
|
final DateTime updatedAt;
|
|
final DateTime? canceledAt;
|
|
final String? canceledReason;
|
|
|
|
const GroupRide({
|
|
required this.id,
|
|
required this.organizerUid,
|
|
required this.organizerHandle,
|
|
required this.organizerDisplayName,
|
|
required this.title,
|
|
this.description = '',
|
|
this.visibility = GroupRideVisibility.friendsOnly,
|
|
this.joinPolicy = GroupRideJoinPolicy.inviteOnly,
|
|
this.status = GroupRideStatus.scheduled,
|
|
required this.scheduledStartAt,
|
|
required this.departureAt,
|
|
this.participantLimit,
|
|
this.participantCount = 1,
|
|
required this.meetingPoint,
|
|
this.destination,
|
|
this.stops = const [],
|
|
required this.createdAt,
|
|
required this.updatedAt,
|
|
this.canceledAt,
|
|
this.canceledReason,
|
|
});
|
|
|
|
bool get isCanceled => status == GroupRideStatus.canceled;
|
|
bool get isCompleted => status == GroupRideStatus.completed;
|
|
bool get isUnderway => status == GroupRideStatus.underway;
|
|
bool get isLiveEligible =>
|
|
status == GroupRideStatus.scheduled ||
|
|
status == GroupRideStatus.assembling ||
|
|
status == GroupRideStatus.underway;
|
|
bool get isFull =>
|
|
participantLimit != null && participantCount >= participantLimit!;
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'id': id,
|
|
'organizerUid': organizerUid,
|
|
'organizerHandle': organizerHandle,
|
|
'organizerDisplayName': organizerDisplayName,
|
|
'title': title,
|
|
'description': description,
|
|
'visibility': visibility.name,
|
|
'joinPolicy': joinPolicy.name,
|
|
'status': status.name,
|
|
'scheduledStartAt': scheduledStartAt.toIso8601String(),
|
|
'departureAt': departureAt.toIso8601String(),
|
|
if (participantLimit != null) 'participantLimit': participantLimit,
|
|
'participantCount': participantCount,
|
|
'meetingPoint': meetingPoint.toJson(),
|
|
if (destination != null) 'destination': destination!.toJson(),
|
|
'stops': stops.map((s) => s.toJson()).toList(),
|
|
'createdAt': createdAt.toIso8601String(),
|
|
'updatedAt': updatedAt.toIso8601String(),
|
|
if (canceledAt != null) 'canceledAt': canceledAt!.toIso8601String(),
|
|
if (canceledReason != null) 'canceledReason': canceledReason,
|
|
};
|
|
|
|
factory GroupRide.fromJson(Map<String, dynamic> json) {
|
|
final created = json['createdAt'] != null
|
|
? DateTime.tryParse(json['createdAt'] as String) ?? DateTime.now()
|
|
: DateTime.now();
|
|
final updated = json['updatedAt'] != null
|
|
? DateTime.tryParse(json['updatedAt'] as String) ?? created
|
|
: created;
|
|
final scheduled = json['scheduledStartAt'] != null
|
|
? DateTime.tryParse(json['scheduledStartAt'] as String) ?? created
|
|
: created;
|
|
final departure = json['departureAt'] != null
|
|
? DateTime.tryParse(json['departureAt'] as String) ?? scheduled
|
|
: scheduled;
|
|
final canceled = json['canceledAt'] != null
|
|
? DateTime.tryParse(json['canceledAt'] as String)
|
|
: null;
|
|
|
|
final stopsList = (json['stops'] as List<dynamic>?)
|
|
?.map((s) => GroupRideLocation.fromJson(s as Map<String, dynamic>))
|
|
.toList() ??
|
|
const [];
|
|
|
|
return GroupRide(
|
|
id: json['id'] as String? ?? '',
|
|
organizerUid: json['organizerUid'] as String? ?? '',
|
|
organizerHandle: json['organizerHandle'] as String? ?? '',
|
|
organizerDisplayName: json['organizerDisplayName'] as String? ?? '',
|
|
title: json['title'] as String? ?? '',
|
|
description: json['description'] as String? ?? '',
|
|
visibility: GroupRideVisibility.values.firstWhere(
|
|
(v) => v.name == (json['visibility'] as String?),
|
|
orElse: () => GroupRideVisibility.friendsOnly,
|
|
),
|
|
joinPolicy: GroupRideJoinPolicy.values.firstWhere(
|
|
(p) => p.name == (json['joinPolicy'] as String?),
|
|
orElse: () => GroupRideJoinPolicy.inviteOnly,
|
|
),
|
|
status: GroupRideStatus.values.firstWhere(
|
|
(s) => s.name == (json['status'] as String?),
|
|
orElse: () => GroupRideStatus.scheduled,
|
|
),
|
|
scheduledStartAt: scheduled,
|
|
departureAt: departure,
|
|
participantLimit: json['participantLimit'] as int?,
|
|
participantCount: json['participantCount'] as int? ?? 1,
|
|
meetingPoint: json['meetingPoint'] != null
|
|
? GroupRideLocation.fromJson(
|
|
json['meetingPoint'] as Map<String, dynamic>)
|
|
: const GroupRideLocation(
|
|
latitude: 0,
|
|
longitude: 0,
|
|
displayName: 'TBD Meetup',
|
|
),
|
|
destination: json['destination'] != null
|
|
? GroupRideLocation.fromJson(
|
|
json['destination'] as Map<String, dynamic>)
|
|
: null,
|
|
stops: stopsList,
|
|
createdAt: created,
|
|
updatedAt: updated,
|
|
canceledAt: canceled,
|
|
canceledReason: json['canceledReason'] as String?,
|
|
);
|
|
}
|
|
|
|
GroupRide copyWith({
|
|
String? id,
|
|
String? organizerUid,
|
|
String? organizerHandle,
|
|
String? organizerDisplayName,
|
|
String? title,
|
|
String? description,
|
|
GroupRideVisibility? visibility,
|
|
GroupRideJoinPolicy? joinPolicy,
|
|
GroupRideStatus? status,
|
|
DateTime? scheduledStartAt,
|
|
DateTime? departureAt,
|
|
int? participantLimit,
|
|
int? participantCount,
|
|
GroupRideLocation? meetingPoint,
|
|
GroupRideLocation? destination,
|
|
List<GroupRideLocation>? stops,
|
|
DateTime? createdAt,
|
|
DateTime? updatedAt,
|
|
DateTime? canceledAt,
|
|
String? canceledReason,
|
|
}) {
|
|
return GroupRide(
|
|
id: id ?? this.id,
|
|
organizerUid: organizerUid ?? this.organizerUid,
|
|
organizerHandle: organizerHandle ?? this.organizerHandle,
|
|
organizerDisplayName: organizerDisplayName ?? this.organizerDisplayName,
|
|
title: title ?? this.title,
|
|
description: description ?? this.description,
|
|
visibility: visibility ?? this.visibility,
|
|
joinPolicy: joinPolicy ?? this.joinPolicy,
|
|
status: status ?? this.status,
|
|
scheduledStartAt: scheduledStartAt ?? this.scheduledStartAt,
|
|
departureAt: departureAt ?? this.departureAt,
|
|
participantLimit: participantLimit ?? this.participantLimit,
|
|
participantCount: participantCount ?? this.participantCount,
|
|
meetingPoint: meetingPoint ?? this.meetingPoint,
|
|
destination: destination ?? this.destination,
|
|
stops: stops ?? this.stops,
|
|
createdAt: createdAt ?? this.createdAt,
|
|
updatedAt: updatedAt ?? this.updatedAt,
|
|
canceledAt: canceledAt ?? this.canceledAt,
|
|
canceledReason: canceledReason ?? this.canceledReason,
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is GroupRide &&
|
|
runtimeType == other.runtimeType &&
|
|
id == other.id &&
|
|
organizerUid == other.organizerUid &&
|
|
title == other.title &&
|
|
status == other.status &&
|
|
scheduledStartAt == other.scheduledStartAt &&
|
|
departureAt == other.departureAt &&
|
|
participantCount == other.participantCount &&
|
|
participantLimit == other.participantLimit;
|
|
|
|
@override
|
|
int get hashCode =>
|
|
id.hashCode ^
|
|
organizerUid.hashCode ^
|
|
title.hashCode ^
|
|
status.hashCode ^
|
|
scheduledStartAt.hashCode ^
|
|
departureAt.hashCode ^
|
|
participantCount.hashCode ^
|
|
participantLimit.hashCode;
|
|
}
|
|
|
|
/// Status of rider in live group ride presence.
|
|
enum LivePresenceStatus {
|
|
riding,
|
|
stopped,
|
|
needFuel,
|
|
regrouping,
|
|
delayed,
|
|
arrived,
|
|
offline;
|
|
|
|
String get label {
|
|
switch (this) {
|
|
case LivePresenceStatus.riding:
|
|
return 'Riding';
|
|
case LivePresenceStatus.stopped:
|
|
return 'Stopped';
|
|
case LivePresenceStatus.needFuel:
|
|
return 'Need Fuel';
|
|
case LivePresenceStatus.regrouping:
|
|
return 'Regroup';
|
|
case LivePresenceStatus.delayed:
|
|
return 'Delayed';
|
|
case LivePresenceStatus.arrived:
|
|
return 'Arrived';
|
|
case LivePresenceStatus.offline:
|
|
return 'Offline';
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Ephemeral live group presence for an accepted group ride participant.
|
|
///
|
|
/// Strictly isolated from public profiles, garage, telemetry bundles, and
|
|
/// persistent user records. Whitelisted coordinates and coarse indicators only.
|
|
@immutable
|
|
class LiveGroupPresence {
|
|
final String userId;
|
|
final String groupRideId;
|
|
final double latitude;
|
|
final double longitude;
|
|
final double? heading;
|
|
final double? speedMph;
|
|
final double? accuracyMeters;
|
|
final DateTime updatedAt;
|
|
final DateTime expiresAt;
|
|
final bool sharingEnabled;
|
|
final LivePresenceStatus status;
|
|
|
|
const LiveGroupPresence({
|
|
required this.userId,
|
|
required this.groupRideId,
|
|
required this.latitude,
|
|
required this.longitude,
|
|
this.heading,
|
|
this.speedMph,
|
|
this.accuracyMeters,
|
|
required this.updatedAt,
|
|
required this.expiresAt,
|
|
this.sharingEnabled = true,
|
|
this.status = LivePresenceStatus.riding,
|
|
});
|
|
|
|
/// Location is considered fresh if updated within the last 60 seconds and sharing is active.
|
|
bool get isFresh =>
|
|
sharingEnabled && DateTime.now().difference(updatedAt).inSeconds < 60;
|
|
|
|
/// Location is considered recent if between 60 and 120 seconds old.
|
|
bool get isRecent {
|
|
final diff = DateTime.now().difference(updatedAt).inSeconds;
|
|
return sharingEnabled && diff >= 60 && diff < 120;
|
|
}
|
|
|
|
/// Elapsed seconds since last update.
|
|
int get secondsAgo => DateTime.now().difference(updatedAt).inSeconds;
|
|
|
|
/// Location is considered stale if over 120 seconds old or sharing was disabled.
|
|
bool get isStale =>
|
|
!sharingEnabled || DateTime.now().difference(updatedAt).inSeconds >= 120;
|
|
|
|
/// Coordinates within valid physical limits.
|
|
bool get isValidCoordinates =>
|
|
latitude >= -90.0 &&
|
|
latitude <= 90.0 &&
|
|
longitude >= -180.0 &&
|
|
longitude <= 180.0;
|
|
|
|
/// Reasonable GPS accuracy (within 100m) if provided.
|
|
bool get isAccurate => accuracyMeters == null || accuracyMeters! <= 100.0;
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'userId': userId,
|
|
'groupRideId': groupRideId,
|
|
'latitude': latitude,
|
|
'longitude': longitude,
|
|
if (heading != null) 'heading': heading,
|
|
if (speedMph != null) 'speedMph': speedMph,
|
|
if (accuracyMeters != null) 'accuracyMeters': accuracyMeters,
|
|
'updatedAt': updatedAt.toIso8601String(),
|
|
'expiresAt': expiresAt.toIso8601String(),
|
|
'sharingEnabled': sharingEnabled,
|
|
'status': status.name,
|
|
};
|
|
|
|
factory LiveGroupPresence.fromJson(Map<String, dynamic> json) {
|
|
final updated = json['updatedAt'] != null
|
|
? DateTime.tryParse(json['updatedAt'] as String) ?? DateTime.now()
|
|
: DateTime.now();
|
|
final expires = json['expiresAt'] != null
|
|
? DateTime.tryParse(json['expiresAt'] as String) ??
|
|
updated.add(const Duration(seconds: 120))
|
|
: updated.add(const Duration(seconds: 120));
|
|
|
|
return LiveGroupPresence(
|
|
userId: json['userId'] as String? ?? '',
|
|
groupRideId: json['groupRideId'] as String? ?? '',
|
|
latitude: (json['latitude'] as num?)?.toDouble() ?? 0.0,
|
|
longitude: (json['longitude'] as num?)?.toDouble() ?? 0.0,
|
|
heading: (json['heading'] as num?)?.toDouble(),
|
|
speedMph: (json['speedMph'] as num?)?.toDouble(),
|
|
accuracyMeters: (json['accuracyMeters'] as num?)?.toDouble(),
|
|
updatedAt: updated,
|
|
expiresAt: expires,
|
|
sharingEnabled: json['sharingEnabled'] as bool? ?? false,
|
|
status: LivePresenceStatus.values.firstWhere(
|
|
(s) => s.name == (json['status'] as String?),
|
|
orElse: () => LivePresenceStatus.offline,
|
|
),
|
|
);
|
|
}
|
|
|
|
LiveGroupPresence copyWith({
|
|
String? userId,
|
|
String? groupRideId,
|
|
double? latitude,
|
|
double? longitude,
|
|
double? heading,
|
|
double? speedMph,
|
|
double? accuracyMeters,
|
|
DateTime? updatedAt,
|
|
DateTime? expiresAt,
|
|
bool? sharingEnabled,
|
|
LivePresenceStatus? status,
|
|
}) {
|
|
return LiveGroupPresence(
|
|
userId: userId ?? this.userId,
|
|
groupRideId: groupRideId ?? this.groupRideId,
|
|
latitude: latitude ?? this.latitude,
|
|
longitude: longitude ?? this.longitude,
|
|
heading: heading ?? this.heading,
|
|
speedMph: speedMph ?? this.speedMph,
|
|
accuracyMeters: accuracyMeters ?? this.accuracyMeters,
|
|
updatedAt: updatedAt ?? this.updatedAt,
|
|
expiresAt: expiresAt ?? this.expiresAt,
|
|
sharingEnabled: sharingEnabled ?? this.sharingEnabled,
|
|
status: status ?? this.status,
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is LiveGroupPresence &&
|
|
runtimeType == other.runtimeType &&
|
|
userId == other.userId &&
|
|
groupRideId == other.groupRideId &&
|
|
latitude == other.latitude &&
|
|
longitude == other.longitude &&
|
|
heading == other.heading &&
|
|
speedMph == other.speedMph &&
|
|
accuracyMeters == other.accuracyMeters &&
|
|
sharingEnabled == other.sharingEnabled &&
|
|
status == other.status;
|
|
|
|
@override
|
|
int get hashCode =>
|
|
userId.hashCode ^
|
|
groupRideId.hashCode ^
|
|
latitude.hashCode ^
|
|
longitude.hashCode ^
|
|
heading.hashCode ^
|
|
speedMph.hashCode ^
|
|
accuracyMeters.hashCode ^
|
|
sharingEnabled.hashCode ^
|
|
status.hashCode;
|
|
}
|