Implement haversine distance sort for list fallback view
epic-geographic-peer-mentor-map-core-services-task-007 — Implement a client-side haversine distance calculation utility within MentorLocationService that takes the coordinator's current location (or map centre) and a list of MentorLocation objects, and returns them sorted by ascending distance in kilometres. This is used by the list fallback view when offline or when the map is unavailable. Include unit tests with known coordinate pairs.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 2 - 518 tasks
Can start after Tier 1 completes
Implementation Notes
Implement as a static method on a HaversineUtils class or as a top-level function in a distance_utils.dart file within the service layer. The formula: a = sin²(Δlat/2) + cos(lat1)·cos(lat2)·sin²(Δlon/2); c = 2·atan2(√a, √(1−a)); d = R·c. All angles must be converted from degrees to radians using the pi constant from dart:math. Use List.sort with a custom comparator that handles null coordinates by treating them as double.infinity.
Do not use geolocator or latlong2 packages for the calculation itself — keep the utility dependency-free. The MentorLocationService can expose this as sortByDistanceFromOrigin(LatLng origin) as a convenience wrapper that delegates to the static utility.
Testing Requirements
All tests in flutter_test (no widget environment needed — pure unit tests). Include parameterised tests for the known Norwegian city pairs listed in the acceptance criteria. Test boundary values: antipodal points (~20004 km), same point (0 km), equator-to-pole. Test null coordinate handling.
Test list stability: two mentors at equal distance should retain their original relative order (stable sort). Verify the function does not modify the input list by checking reference equality before and after the call. Target 100% line coverage on the haversine utility function.
The dual BLoC state machines (map view state + filter state) may introduce subtle synchronisation bugs where filter changes do not correctly re-trigger viewport queries, causing stale data to appear on the map.
Mitigation & Contingency
Mitigation: Define all BLoC state transitions in a state diagram before implementation. Use flutter_bloc's BlocObserver in development mode to log every state transition. Write explicit unit tests for filter-change → re-query transitions.
Contingency: If state synchronisation bugs appear in integration testing, refactor to a single unified BLoC that owns both map viewport state and filter state, eliminating cross-BLoC dependencies.
Cached mentor location data may become stale (mentors move, pause, or revoke consent) and coordinators in offline mode could be shown incorrect mentor information, leading to wasted outreach.
Mitigation & Contingency
Mitigation: Display a clear timestamp on cached data indicating when it was last synced. Set cache TTL to 24 hours and show an 'offline — data from [date]' banner. Revoked consent removes the mentor from the cache on next successful sync via contact-cache-sync-repository.
Contingency: If cache staleness causes user complaints, reduce TTL to 4 hours and implement background sync on app foreground. Accept that very-recently-revoked mentors may appear briefly in offline mode — document this as a known limitation in the privacy policy.