agus_maps_flutter 0.1.8
agus_maps_flutter: ^0.1.8 copied to clipboard
High-performance offline vector maps for Flutter using the CoMaps rendering engine.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'dart:async';
import 'dart:io';
import 'package:agus_maps_flutter/agus_maps_flutter.dart' as agus_maps_flutter;
import 'package:agus_maps_flutter/mwm_storage.dart';
import 'package:path_provider/path_provider.dart';
import 'about_tab.dart';
import 'downloads_tab.dart';
import 'settings_tab.dart';
void main() {
// Ensure Flutter bindings are initialized before using platform channels
// (required for SharedPreferences, path_provider, etc.)
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
/// A favorite location entry.
class FavoriteLocation {
final String name;
final double lat;
final double lon;
final int zoom;
const FavoriteLocation({
required this.name,
required this.lat,
required this.lon,
required this.zoom,
});
}
/// Hardcoded favorite locations.
const List<FavoriteLocation> kFavorites = [
FavoriteLocation(
name: 'Gibraltar',
lat: 36.1407,
lon: -5.3535,
zoom: 14,
),
FavoriteLocation(
name: 'Philippines',
lat: 11.840743046600755,
lon: 123.11028882297192,
zoom: 6,
),
];
/// Default location when app starts (Philippines).
const FavoriteLocation kDefaultLocation = FavoriteLocation(
name: 'Philippines',
lat: 11.840743046600755,
lon: 123.11028882297192,
zoom: 6,
);
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String _status = 'Initializing...';
String _debug = '';
bool _dataReady = false;
int _currentTabIndex = 0; // Start on Map tab
int? _bundledMwmVersion;
final agus_maps_flutter.AgusMapController _mapController =
agus_maps_flutter.AgusMapController();
// Store map paths for registration after Framework is ready
final List<String> _mapPathsToRegister = [];
// MWM storage for tracking downloaded maps
MwmStorage? _mwmStorage;
@override
void initState() {
super.initState();
// Defer initialization to after the first frame is rendered.
// This ensures Flutter platform channels (SharedPreferences, path_provider)
// are fully registered before we try to use them. On Android with Impeller,
// platform channels may not be ready during initState().
SchedulerBinding.instance.addPostFrameCallback((_) {
_initData();
});
}
Future<void> _initData() async {
try {
_log('Starting initialization...');
// Initialize MWM storage
_log('Initializing MWM storage...');
_mwmStorage = await MwmStorage.create();
// Clean up any partial downloads from interrupted sessions.
// These are .mwm.download files that were being written when the app was killed.
// If not cleaned up, RegisterAllMaps() would try to load them and crash.
_log('Cleaning up partial downloads...');
await _cleanupPartialDownloads();
// Validate existing metadata against actual files on disk.
// After reinstall, SharedPreferences may persist but files are deleted.
_log('Validating stored MWM metadata...');
final orphanedRegions = await _mwmStorage!.getOrphanedRegions();
if (orphanedRegions.isNotEmpty) {
_log(
'Found ${orphanedRegions.length} orphaned regions: $orphanedRegions');
_log('Pruning orphaned metadata...');
await _mwmStorage!.pruneOrphaned();
_log('Orphaned metadata pruned.');
} else {
_log('All stored metadata is valid.');
}
// 1. Extract map files and store paths for later registration
_log('Extracting World.mwm...');
final worldPath =
await agus_maps_flutter.extractMap('assets/maps/World.mwm');
_mapPathsToRegister.add(worldPath);
_log('Extracting WorldCoasts.mwm...');
final coastsPath =
await agus_maps_flutter.extractMap('assets/maps/WorldCoasts.mwm');
_mapPathsToRegister.add(coastsPath);
_log('Extracting Gibraltar.mwm...');
final gibraltarPath =
await agus_maps_flutter.extractMap('assets/maps/Gibraltar.mwm');
_mapPathsToRegister.add(gibraltarPath);
_log('Map paths: $_mapPathsToRegister');
// Record bundled maps in storage (if not already there)
final worldFile = File(worldPath);
final coastsFile = File(coastsPath);
final gibraltarFile = File(gibraltarPath);
if (!_mwmStorage!.isDownloaded('World')) {
await _mwmStorage!.upsert(MwmMetadata(
regionName: 'World',
snapshotVersion: 'bundled',
fileSize: await worldFile.length(),
downloadDate: DateTime.now(),
filePath: worldPath,
isBundled: true,
));
}
if (!_mwmStorage!.isDownloaded('WorldCoasts')) {
await _mwmStorage!.upsert(MwmMetadata(
regionName: 'WorldCoasts',
snapshotVersion: 'bundled',
fileSize: await coastsFile.length(),
downloadDate: DateTime.now(),
filePath: coastsPath,
isBundled: true,
));
}
if (!_mwmStorage!.isDownloaded('Gibraltar')) {
await _mwmStorage!.upsert(MwmMetadata(
regionName: 'Gibraltar',
snapshotVersion: 'bundled',
fileSize: await gibraltarFile.length(),
downloadDate: DateTime.now(),
filePath: gibraltarPath,
isBundled: true,
));
}
// 2. Extract ICU data for transliteration
_log('Extracting icudt75l.dat...');
await agus_maps_flutter.extractMap('assets/maps/icudt75l.dat');
// 3. Extract CoMaps data files (classificator.txt, types.txt, etc.)
_log('Extracting data files...');
String dataPath = await agus_maps_flutter.extractDataFiles();
_log('Data path: $dataPath');
_bundledMwmVersion = await _readBundledMwmVersion(dataPath);
_log('Bundled MWM version: ${_bundledMwmVersion ?? 'unknown'}');
// 4. Initialize with extracted data files
_log('Calling initWithPaths()...');
agus_maps_flutter.initWithPaths(dataPath, dataPath);
_log('initWithPaths() complete');
// NOTE: Don't call loadMap() here - Framework isn't ready yet!
// Maps will be registered in _onMapReady() after surface creation.
_log('Maps will be registered after surface creation...');
if (!mounted) return;
setState(() {
_status = 'Data ready - creating map...';
_dataReady = true;
});
} catch (e, stackTrace) {
_log('ERROR: $e\n$stackTrace');
if (!mounted) return;
setState(() {
_status = 'Error: $e';
});
}
}
void _onMapReady() {
// Kick off async work without blocking the widget callback.
unawaited(_onMapReadyAsync());
}
Future<void> _onMapReadyAsync() async {
_log('Map surface ready! Registering maps...');
final bundledVersion = _bundledMwmVersion;
if (bundledVersion == null) {
_log('WARNING: bundled MWM version unknown; registrations may fail.');
}
// Register bundled maps (extracted during init).
// If a bundled MWM is too old for the current engine (RegResult=2),
// delete it and re-extract from assets (updated in repo), then retry.
for (final path in _mapPathsToRegister) {
final result = bundledVersion != null
? agus_maps_flutter.registerSingleMapWithVersion(path, bundledVersion)
: agus_maps_flutter.registerSingleMap(path);
_log('Registered bundled $path: result=$result');
// 2 == MwmSet::RegResult::VersionTooOld
if (result == 2) {
final fileName = File(path).uri.pathSegments.last;
final assetPath = 'assets/maps/$fileName';
_log('Bundled $fileName is too old; re-extracting from $assetPath...');
try {
// Force re-extraction by removing the previously extracted file.
final f = File(path);
if (await f.exists()) {
await f.delete();
}
final newPath = await agus_maps_flutter.extractMap(assetPath);
final retry = bundledVersion != null
? agus_maps_flutter.registerSingleMapWithVersion(
newPath,
bundledVersion,
)
: agus_maps_flutter.registerSingleMap(newPath);
_log('Re-registered bundled $newPath: result=$retry');
} catch (e, st) {
_log('Failed to re-extract/re-register $fileName: $e\n$st');
}
}
}
// Re-register all previously downloaded maps from MwmStorage
// This is crucial: downloaded maps are only stored as metadata,
// they need to be re-registered with the native engine on each app start
if (_mwmStorage != null) {
final allMaps = _mwmStorage!.getAll();
_log('Re-registering ${allMaps.length} maps from storage...');
for (final metadata in allMaps) {
// Skip bundled maps (already registered above)
if (metadata.isBundled) continue;
_log(
'Re-registering downloaded: ${metadata.regionName} at ${metadata.filePath}');
final parsed = int.tryParse(metadata.snapshotVersion);
final result = parsed != null
? agus_maps_flutter.registerSingleMapWithVersion(
metadata.filePath,
parsed,
)
: agus_maps_flutter.registerSingleMap(metadata.filePath);
_log(' Result: $result');
}
}
// Force invalidate the map viewport after registering maps
// This ensures the DrapeEngine reloads tiles with the newly registered maps
_log('Invalidating map viewport...');
agus_maps_flutter.invalidateMap();
// Force a complete redraw to ensure tiles are loaded for newly registered maps
// This is necessary because maps are registered AFTER DrapeEngine initialization,
// so the engine may have calculated tile coverage before maps were available
_log('Forcing complete tile reload...');
agus_maps_flutter.forceRedraw();
// Debug: List all registered MWMs and check Manila coverage
_log('Debug: Listing all registered MWMs...');
agus_maps_flutter.debugListMwms();
// Check Manila, Philippines (14.5995, 120.9842)
_log('Debug: Checking Manila coverage...');
agus_maps_flutter.debugCheckPoint(14.5995, 120.9842);
if (mounted) {
setState(() {
_status = 'Map ready!';
});
}
}
void _log(String msg) {
debugPrint('[AgusDemo] $msg');
if (mounted) {
setState(() {
_debug += '$msg\n';
});
}
}
Future<int?> _readBundledMwmVersion(String dataPath) async {
try {
final file = File('$dataPath/countries.txt');
if (!await file.exists()) {
return null;
}
final contents = await file.readAsString();
// countries.txt is JSON: { "v": 251209, "id": "Countries", ... }
// Parse the "v" field which contains the MWM snapshot version.
final match = RegExp(r'"v"\s*:\s*(\d+)').firstMatch(contents);
if (match != null) {
return int.tryParse(match.group(1)!);
}
return null;
} catch (_) {
return null;
}
}
/// Clean up partial downloads from interrupted sessions.
///
/// When the app is killed during a download, the partial .mwm.download file
/// remains on disk. If not cleaned up, RegisterAllMaps() might crash trying
/// to load corrupted/incomplete map files.
Future<void> _cleanupPartialDownloads() async {
try {
final dir = await getApplicationDocumentsDirectory();
final files = dir.listSync();
int cleanedCount = 0;
for (final entity in files) {
if (entity is File && entity.path.endsWith('.mwm.download')) {
_log('Removing partial download: ${entity.path}');
await entity.delete();
cleanedCount++;
}
}
if (cleanedCount > 0) {
_log('Cleaned up $cleanedCount partial download(s)');
}
} catch (e) {
_log('Warning: Failed to clean up partial downloads: $e');
// Don't rethrow - cleanup failure shouldn't prevent app startup
}
}
void _onFavoriteSelected(FavoriteLocation favorite) {
// Navigate to the map and move to the selected location
_mapController.moveToLocation(favorite.lat, favorite.lon, favorite.zoom);
setState(() {
_currentTabIndex = 0; // Switch to Map tab
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: Scaffold(
body: SafeArea(
// Use IndexedStack to keep all tabs alive (especially the map)
// This prevents the map from being unmounted/remounted when switching tabs
child: IndexedStack(
index: _currentTabIndex,
children: [
_buildMapTab(),
_buildFavoritesTab(),
_buildDownloadsTab(),
const SettingsTab(),
const AboutTab(),
],
),
),
bottomNavigationBar: NavigationBar(
selectedIndex: _currentTabIndex,
onDestinationSelected: (index) {
setState(() {
_currentTabIndex = index;
});
},
destinations: const [
NavigationDestination(
icon: Icon(Icons.map_outlined),
selectedIcon: Icon(Icons.map),
label: 'Map',
),
NavigationDestination(
icon: Icon(Icons.favorite_border),
selectedIcon: Icon(Icons.favorite),
label: 'Favorites',
),
NavigationDestination(
icon: Icon(Icons.download_outlined),
selectedIcon: Icon(Icons.download),
label: 'Downloads',
),
NavigationDestination(
icon: Icon(Icons.settings_outlined),
selectedIcon: Icon(Icons.settings),
label: 'Settings',
),
NavigationDestination(
icon: Icon(Icons.info_outline),
selectedIcon: Icon(Icons.info),
label: 'About',
),
],
),
),
);
}
/// Full-screen map tab.
Widget _buildMapTab() {
if (!_dataReady) {
return Container(
color: Colors.grey[200],
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(_status),
const SizedBox(height: 16),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Text(
_debug,
style:
const TextStyle(fontSize: 10, fontFamily: 'monospace'),
),
),
),
],
),
),
);
}
return agus_maps_flutter.AgusMap(
initialLat: kDefaultLocation.lat,
initialLon: kDefaultLocation.lon,
initialZoom: kDefaultLocation.zoom,
onMapReady: _onMapReady,
controller: _mapController,
isVisible: _currentTabIndex == 0, // Only resize when map tab is active
);
}
/// Full-screen favorites tab.
Widget _buildFavoritesTab() {
return Column(
children: [
// Header
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
border: Border(
bottom: BorderSide(color: Theme.of(context).dividerColor),
),
),
child: Row(
children: [
Icon(Icons.favorite,
color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 8),
Text(
'Favorites',
style: Theme.of(context).textTheme.titleLarge,
),
],
),
),
// List
Expanded(
child: ListView.builder(
itemCount: kFavorites.length,
itemBuilder: (context, index) {
final favorite = kFavorites[index];
return ListTile(
leading: const CircleAvatar(
child: Icon(Icons.location_on),
),
title: Text(favorite.name),
subtitle: Text(
'${favorite.lat.toStringAsFixed(4)}, ${favorite.lon.toStringAsFixed(4)}',
),
trailing: Text(
'Zoom ${favorite.zoom}',
style: Theme.of(context).textTheme.bodySmall,
),
onTap: () => _onFavoriteSelected(favorite),
);
},
),
),
],
);
}
/// Full-screen downloads tab.
Widget _buildDownloadsTab() {
if (_mwmStorage == null) {
return const Center(child: CircularProgressIndicator());
}
return DownloadsTab(
mwmStorage: _mwmStorage!,
isVisible: _currentTabIndex == 2, // Downloads tab is index 2
onMapsChanged: () {
setState(() {});
},
);
}
}