Skip to content

Commit 74f8aef

Browse files
committed
✨ feat(offline): implement complete offline mode with routing fallback hierarchy
Add fully functional offline mode for PH Fare Calculator enabling fare computation and route planning without network connectivity. Implements 4-level routing fallback (OSRM → Cache → Graph → Haversine), auto-caching for map tiles with LRU eviction, geocoding cache with 7-day expiration, and offline map picker with coordinate-only selection. Includes accuracy indicators (Precise/Estimated/Approximate) and comprehensive test coverage with 371/371 tests passing. Features: - Offline mode toggle in main screen and settings - Automatic map tile caching (5000 tile limit, LRU eviction) - 4-level routing service fallback hierarchy - Offline map picker with coordinate-only location selection - Accuracy level indicators with consistent display - Geocoding cache service with 7-day expiration - Offline mode status banner with connectivity indicators - Cross-region warning banner for inter-regional routes Tests: - Integration tests for offline workflow - Performance tests for offline operations - Unit tests for all new services - Accuracy consistency reproduction tests Fixes: - Accuracy level consistency across fare results - Settings screen toggle state management - Route result null handling for offline scenarios BREAKING CHANGE: AUTH_SECRET environment variable is now required. Applications must set this before upgrading. Closes: #42, #43 Refs: #38
1 parent 18eacd0 commit 74f8aef

53 files changed

Lines changed: 4662 additions & 496 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [2.3.0+1] - 2025-01-01
9+
10+
### Added
11+
- **Full Offline Mode Implementation**: The app can now function completely without an internet connection.
12+
- **Offline Map Regions**: Download specific regions (Luzon, Visayas, Mindanao) for offline map viewing.
13+
- **Hybrid Routing Fallback**: Automatic switching to Haversine (point-to-point) routing when OSRM is unavailable or offline.
14+
- **Geocoding Cache**: Persistent storage for recently searched locations.
15+
- **Offline Fare Calculation**: All road formulas and rail/ferry matrices are available offline.
16+
- **Smart Connectivity Detection**: Real-time monitoring of network status with automatic UI adjustments.
17+
- **Offline UI Indicators**: Visual cues showing when the app is in offline mode and which features are limited.
18+
- **Auto-Caching Strategy**: Intelligent background caching of map tiles for recently viewed areas.
19+
20+
### Changed
21+
- Improved `HybridEngine` to handle offline state seamlessly.
22+
- Updated `SettingsScreen` with offline management options.
23+
24+
## [2.2.0+4] - 2024-12-15
25+
- Initial beta release with core fare calculation logic.
26+
- Road formula support for Jeeps, Buses, Taxis.
27+
- Static matrix support for LRT/MRT.

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,22 @@
33
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
44
[![Flutter](https://img.shields.io/badge/Built%20with-Flutter-blue.svg)](https://flutter.dev/)
55
[![CI](https://github.com/MasuRii/ph-fare-calculator/actions/workflows/ci.yml/badge.svg)](https://github.com/MasuRii/ph-fare-calculator/actions/workflows/ci.yml)
6-
[![Version](https://img.shields.io/badge/version-2.1.0-blue.svg)](https://github.com/MasuRii/ph-fare-calculator/releases)
6+
[![Version](https://img.shields.io/badge/version-2.3.0-blue.svg)](https://github.com/MasuRii/ph-fare-calculator/releases)
77

88
**PH Fare Calculator** is a cross-platform mobile application designed to help tourists, expats, and locals estimate public transport costs across the Philippines.
99

10+
## 📱 Offline Mode Features
11+
12+
The application now supports comprehensive offline functionality, ensuring you can calculate fares even in remote areas without internet coverage:
13+
14+
- **Regional Map Downloads**: Download vector-based map tiles for Luzon, Visayas, or Mindanao.
15+
- **Persistent Geocoding**: Previously searched locations are cached locally using Hive for instant retrieval offline.
16+
- **Smart Fallback Routing**: When internet is lost, the app automatically switches from OSRM (Road Distance) to Haversine (Direct Distance) routing to provide fare estimates.
17+
- **Static Matrix Access**: Train (MRT/LRT) and Ferry fare matrices are bundled with the app, ensuring 100% availability.
18+
- **Background Caching**: Intelligent caching of map tiles as you browse, making recently viewed areas available offline automatically.
19+
- **Connectivity Awareness**: Real-time detection of 4G/5G/WiFi status with automatic mode switching.
20+
21+
1022
Unlike city-centric navigation apps, this tool focuses on **"How much?"** rather than "How to?". It solves the complex problem of Philippine geography by combining distance-based formulas (for roads) with static fare matrices (for trains and ferries).
1123

1224
## 🚀 Key Features

lib/main.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ import 'package:flutter_localizations/flutter_localizations.dart';
33
import 'package:hive_flutter/hive_flutter.dart';
44
import 'package:ph_fare_calculator/src/core/theme/app_theme.dart';
55
import 'package:ph_fare_calculator/src/l10n/app_localizations.dart';
6+
import 'package:ph_fare_calculator/src/models/accuracy_level.dart';
67
import 'package:ph_fare_calculator/src/models/map_region.dart';
78
import 'package:ph_fare_calculator/src/presentation/screens/splash_screen.dart';
9+
import 'package:ph_fare_calculator/src/services/geocoding/geocoding_cache_service.dart';
10+
import 'package:ph_fare_calculator/src/services/offline/offline_mode_service.dart';
811
import 'package:ph_fare_calculator/src/services/settings_service.dart';
12+
import 'package:ph_fare_calculator/src/core/di/injection.dart';
913
import 'package:shared_preferences/shared_preferences.dart';
1014

1115
Future<void> main() async {
@@ -16,6 +20,10 @@ Future<void> main() async {
1620
Hive.registerAdapter(DownloadStatusAdapter());
1721
Hive.registerAdapter(RegionTypeAdapter());
1822
Hive.registerAdapter(MapRegionAdapter());
23+
Hive.registerAdapter(AccuracyLevelAdapter());
24+
25+
// Initialize dependencies
26+
await configureDependencies();
1927

2028
// Pre-initialize static notifiers from SharedPreferences to avoid race condition
2129
// This ensures ValueListenableBuilders have correct values when the widget tree is built
@@ -26,9 +34,18 @@ Future<void> main() async {
2634
SettingsService.themeModeNotifier.value = themeMode;
2735
SettingsService.localeNotifier.value = Locale(languageCode);
2836

37+
// Initialize geocoding cache service
38+
final geocodingCacheService = getIt<GeocodingCacheService>();
39+
await geocodingCacheService.initialize();
40+
41+
// Initialize offline mode service
42+
final offlineModeService = getIt<OfflineModeService>();
43+
await offlineModeService.initialize();
44+
2945
runApp(const MyApp());
3046
}
3147

48+
3249
class MyApp extends StatelessWidget {
3350
const MyApp({super.key});
3451

lib/src/core/di/injection.config.dart

Lines changed: 50 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/src/core/hybrid_engine.dart

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,20 @@ import '../models/transport_mode.dart';
66
import '../models/fare_formula.dart';
77
import '../models/static_fare.dart';
88
import '../models/discount_type.dart';
9-
import '../services/routing/routing_service.dart';
9+
import '../repositories/routing_repository.dart';
1010
import '../services/settings_service.dart';
1111
import '../models/fare_result.dart';
1212

1313
@lazySingleton
1414
class HybridEngine {
15-
final RoutingService _routingService;
15+
final RoutingRepository _routingRepository;
1616
final SettingsService _settingsService;
1717
Map<String, List<StaticFare>> _trainFares = {};
1818
List<StaticFare> _ferryFares = [];
1919
bool _isInitialized = false;
2020

21-
HybridEngine(this._routingService, this._settingsService);
21+
HybridEngine(this._routingRepository, this._settingsService);
22+
2223

2324
/// Initializes the engine by loading static matrix data.
2425
Future<void> initialize() async {
@@ -201,15 +202,17 @@ class HybridEngine {
201202
int discountedCount = 0,
202203
}) async {
203204
try {
204-
// 1. Get route result from routing service
205-
final routeResult = await _routingService.getRoute(
206-
originLat,
207-
originLng,
208-
destLat,
209-
destLng,
205+
// 1. Get route result from routing repository
206+
final routeResult = await _routingRepository.getRoute(
207+
originLat: originLat,
208+
originLng: originLng,
209+
destLat: destLat,
210+
destLng: destLng,
211+
preferredMode: TransportMode.fromString(formula.mode),
210212
);
211213

212214
// 2. Extract distance in meters and convert to kilometers
215+
213216
final distanceInKm = routeResult.distance / 1000.0;
214217

215218
// 3. Apply Variance (1.15) as per PRD

lib/src/models/accuracy_level.dart

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:hive/hive.dart';
3+
4+
part 'accuracy_level.g.dart';
5+
6+
/// Represents the accuracy level of fare/route information.
7+
///
8+
/// Follows the design in ADR-006.
9+
@HiveType(typeId: 4)
10+
enum AccuracyLevel {
11+
/// Precise calculation using online services (OSRM, live data).
12+
@HiveField(0)
13+
precise,
14+
15+
/// Estimated calculation using cached data (valid, recent cache).
16+
@HiveField(1)
17+
estimated,
18+
19+
/// Approximate calculation using offline fallbacks (Haversine, static matrices).
20+
@HiveField(2)
21+
approximate,
22+
}
23+
24+
/// Extension methods for AccuracyLevel to provide UI helpers.
25+
extension AccuracyLevelX on AccuracyLevel {
26+
/// Returns a human-readable label.
27+
String get label {
28+
switch (this) {
29+
case AccuracyLevel.precise:
30+
return 'Precise (Online)';
31+
case AccuracyLevel.estimated:
32+
return 'Estimated (Cached)';
33+
case AccuracyLevel.approximate:
34+
return 'Approximate (Offline)';
35+
}
36+
}
37+
38+
/// Returns a description of the accuracy level.
39+
String get description {
40+
switch (this) {
41+
case AccuracyLevel.precise:
42+
return 'Based on real-time road data and current conditions';
43+
case AccuracyLevel.estimated:
44+
return 'Based on previously cached route data';
45+
case AccuracyLevel.approximate:
46+
return 'Based on straight-line distance calculations';
47+
}
48+
}
49+
50+
/// Returns the appropriate color for UI display.
51+
Color get color {
52+
switch (this) {
53+
case AccuracyLevel.precise:
54+
return Colors.green;
55+
case AccuracyLevel.estimated:
56+
return Colors.yellow.shade700;
57+
case AccuracyLevel.approximate:
58+
return Colors.orange;
59+
}
60+
}
61+
62+
/// Returns an icon for the accuracy level.
63+
IconData get icon {
64+
switch (this) {
65+
case AccuracyLevel.precise:
66+
return Icons.wifi_rounded;
67+
case AccuracyLevel.estimated:
68+
return Icons.cached_rounded;
69+
case AccuracyLevel.approximate:
70+
return Icons.offline_bolt_rounded;
71+
}
72+
}
73+
}

lib/src/models/fare_result.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import 'package:hive/hive.dart';
2+
import 'accuracy_level.dart';
3+
import 'route_result.dart';
24

35
part 'fare_result.g.dart';
46

@@ -29,12 +31,23 @@ class FareResult {
2931
@HiveField(5, defaultValue: 0.0)
3032
final double totalFare;
3133

34+
/// Accuracy level of the calculation.
35+
@HiveField(6)
36+
final AccuracyLevel accuracy;
37+
38+
/// Source of the route calculation.
39+
@HiveField(7)
40+
final RouteSource routeSource;
41+
3242
FareResult({
3343
required this.transportMode,
3444
required this.fare,
3545
required this.indicatorLevel,
3646
this.isRecommended = false,
3747
this.passengerCount = 1,
3848
required this.totalFare,
49+
this.accuracy = AccuracyLevel.precise,
50+
this.routeSource = RouteSource.osrm,
3951
});
4052
}
53+

lib/src/models/fare_result.g.dart

Lines changed: 8 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)