import '../../core/region/geo.dart'; /// A place the user has named and kept. /// /// The anchor everything location-shaped hangs off: which part of the region the /// radar map opens on, and — once the worker publishes them — which alert zone /// and which rain cell the notifications for this place should follow. /// /// A free point rather than a station: people think in terms of home and work, /// not in terms of which weather station happens to represent them. The nearest /// station is derived from the point when readings are shown, so the user can /// always see how far away the measurement actually came from. class SavedPlace { const SavedPlace({ required this.id, required this.name, required this.point, required this.createdAt, }); factory SavedPlace.fromJson(Map json) { final id = json['id']; final name = json['name']; final longitude = json['lng']; final latitude = json['lat']; final createdAt = json['createdAt']; if (id is! String || id.isEmpty) { throw const FormatException('saved place id must be a non-empty string'); } if (name is! String || name.isEmpty) { throw const FormatException( 'saved place name must be a non-empty string', ); } if (longitude is! num || latitude is! num) { throw const FormatException('saved place coordinates must be numbers'); } if (createdAt is! int) { throw const FormatException( 'saved place createdAt must be an epoch in ms', ); } return SavedPlace( id: id, name: name, point: GeoPoint(longitude.toDouble(), latitude.toDouble()), createdAt: DateTime.fromMillisecondsSinceEpoch(createdAt, isUtc: true), ); } /// Longest name the UI will accept. /// /// Not a storage limit — it keeps a name readable in the one-line list and in /// a notification title, where a long string would simply be truncated /// somewhere unhelpful. static const int maxNameLength = 40; final String id; final String name; final GeoPoint point; final DateTime createdAt; Map toJson() => { 'id': id, 'name': name, 'lng': point.longitude, 'lat': point.latitude, 'createdAt': createdAt.millisecondsSinceEpoch, }; SavedPlace copyWith({String? name, GeoPoint? point}) => SavedPlace( id: id, name: name ?? this.name, point: point ?? this.point, createdAt: createdAt, ); @override String toString() => 'SavedPlace($id, $name, $point)'; @override bool operator ==(Object other) => other is SavedPlace && other.id == id && other.name == name && other.point == point && other.createdAt == createdAt; @override int get hashCode => Object.hash(id, name, point, createdAt); } /// Why a place could not be saved. /// /// Modelled rather than returned as a bare bool so the UI can say which rule was /// broken instead of a generic failure. enum SavedPlaceRejection { /// The name was empty or only whitespace. emptyName, /// The name exceeded [SavedPlace.maxNameLength]. nameTooLong, /// The point lies outside the region the app has data for. outsideRegion, /// Another saved place already uses that name. duplicateName, } class SavedPlaceException implements Exception { const SavedPlaceException(this.rejection); final SavedPlaceRejection rejection; @override String toString() => 'SavedPlaceException(${rejection.name})'; }