hamuga_imap_sdk 0.0.13
hamuga_imap_sdk: ^0.0.13 copied to clipboard
Official Hamuga Map SDK for Flutter. Provides a MapLibre-powered map widget with built-in search, auto-suggestions, and secure API key handling through a local tile proxy.
example/lib/main.dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:hamuga_imap_sdk/hamuga_imap_sdk.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
const HamugaGeofenceCircle _kMockOfficeGeofence = HamugaGeofenceCircle(
centerLat: 47.9131732,
centerLon: 106.9267709,
radiusM: 50,
);
String? _readConfigValue(String key) {
final dartDefine = String.fromEnvironment(key).trim();
if (dartDefine.isNotEmpty) {
return dartDefine;
}
final envValue = (dotenv.isInitialized ? dotenv.env[key] : null)?.trim();
if (envValue == null || envValue.isEmpty) {
return null;
}
return envValue;
}
String? _readAnyConfigValue(List<String> keys) {
for (final key in keys) {
final value = _readConfigValue(key);
if (value != null && value.isNotEmpty) {
return value;
}
}
return null;
}
double? _readConfigDouble(String key) {
final value = _readConfigValue(key);
if (value == null) {
return null;
}
return double.tryParse(value);
}
bool? _parseConfigBool(String value) {
switch (value.trim().toLowerCase()) {
case '1':
case 'true':
case 'yes':
case 'on':
return true;
case '0':
case 'false':
case 'no':
case 'off':
return false;
}
return null;
}
bool? _readConfigBool(String key) {
final value = _readConfigValue(key);
if (value == null) {
return null;
}
return _parseConfigBool(value);
}
bool _readAnyConfigBool(List<String> keys, {required bool defaultValue}) {
for (final key in keys) {
final value = _readConfigBool(key);
if (value != null) {
return value;
}
}
return defaultValue;
}
Future<void> initializeApp({String? apiKeyOverride}) async {
await dotenv.load(fileName: ".env").catchError((_) {});
final override = apiKeyOverride?.trim();
final dartDefine = const String.fromEnvironment('HAMUGA_API_KEY').trim();
final envFile = (dotenv.isInitialized ? dotenv.env['HAMUGA_API_KEY'] : null)
?.trim();
String? apiKey;
String? source;
if (override != null && override.isNotEmpty) {
apiKey = override;
source = 'override';
} else if (dartDefine.isNotEmpty) {
apiKey = dartDefine;
source = '--dart-define';
} else if (envFile != null && envFile.isNotEmpty) {
apiKey = envFile;
source = '.env file';
}
if (apiKey == null) {
debugPrint('[Example] Failed to resolve HAMUGA_API_KEY from any source.');
throw StateError(
'HAMUGA_API_KEY is missing. Provide it via --dart-define=HAMUGA_API_KEY=... or add it to example/.env.',
);
}
debugPrint(
'[Example] Using API key from $source: ${apiKey.substring(0, 4)}***${apiKey.substring(apiKey.length - 4)}',
);
HamugaApi.initialize(
apiKey: apiKey,
geofenceBaseUrl: _readAnyConfigValue(const [
'GEOFENCE_BASE_URL',
'GEOfENCE_BASE_URL',
]),
);
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeApp();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.white,
surface: Colors.white,
),
useMaterial3: true,
),
debugShowCheckedModeBanner: false,
home: const MapDemoPage(),
);
}
}
class MapDemoPage extends StatefulWidget {
const MapDemoPage({super.key});
@override
State<MapDemoPage> createState() => _MapDemoPageState();
}
class _MapDemoPageState extends State<MapDemoPage> {
final HamugaImapController _controller = HamugaImapController();
bool _isProxyReady = false;
String? _styleJson;
String? _error;
final HamugaImapUiTheme _uiTheme = const HamugaImapUiTheme();
@override
void initState() {
super.initState();
_initTileProxy();
}
@override
void dispose() {
HamugaTileProxy.instance.stop();
super.dispose();
}
Future<void> _initTileProxy() async {
try {
// Start the tile proxy. It will automatically use the API key
// set in HamugaApi.initialize().
await HamugaTileProxy.instance.start();
if (!mounted) return;
final styleJson = HamugaTileProxy.instance.proxyStyleJson;
if (styleJson != null) {
if (!mounted) return;
setState(() {
_isProxyReady = true;
_styleJson = styleJson;
});
} else {
if (!mounted) return;
setState(() {
_error = 'Failed to fetch map style';
});
}
} catch (e) {
if (!mounted) return;
setState(() {
_error = 'Error starting proxy: $e';
});
}
}
@override
Widget build(BuildContext context) => Scaffold(
backgroundColor: _uiTheme.colors.backgroundLight,
body: _buildBody(),
);
Widget _buildBody() {
if (_error != null) {
return _buildErrorState();
}
final officeGeofence = _resolveOfficeGeofence();
final geofenceOptions = _buildGeofenceOptions(officeGeofence);
final config = _buildMapConfig(officeGeofence);
return Stack(
children: [
HamugaImap(
key: ValueKey(_styleJson ?? HamugaImapConfig.defaultStyleUri),
controller: _controller,
config: config,
search: true,
options: HamugaImapOptions(
compassViewPosition: CompassViewPosition.topRight,
compassViewMargins: const Point(20, 64),
showZoomControls: true,
showCompassControl: false,
showMyLocationButton: true,
geofence: geofenceOptions,
),
onMapReady: (mapController) {
debugPrint(
'[Example] Map is ready. officeGeofence='
'${officeGeofence.centerLat},${officeGeofence.centerLon} r=${officeGeofence.radiusM} '
'onlyDuringWorkWindows=${geofenceOptions.onlyDuringWorkWindows}',
);
},
),
if (!_isProxyReady || _styleJson == null) _buildLoadingState(),
],
);
}
HamugaImapConfig _buildMapConfig(HamugaGeofenceCircle officeGeofence) {
return HamugaImapConfig(
styleUri: _styleJson ?? HamugaImapConfig.defaultStyleUri,
initialCameraPosition: CameraPosition(
target: officeGeofence.center,
zoom: _zoomForOfficeRadius(officeGeofence.radiusM),
),
);
}
double _zoomForOfficeRadius(double radiusM) {
if (radiusM <= 60) {
return 16.8;
}
if (radiusM <= 120) {
return 16.2;
}
if (radiusM <= 250) {
return 15.5;
}
if (radiusM <= 500) {
return 14.8;
}
if (radiusM <= 1000) {
return 14.0;
}
return 13.2;
}
HamugaGeofenceCircle _resolveOfficeGeofence() {
final officeLat = _readConfigDouble('OFFICE_LAT');
final officeLon = _readConfigDouble('OFFICE_LON');
final officeRadius = _readConfigDouble('OFFICE_RADIUS');
if (officeLat != null && officeLon != null && officeRadius != null) {
return HamugaGeofenceCircle(
centerLat: officeLat,
centerLon: officeLon,
radiusM: officeRadius,
);
}
return _kMockOfficeGeofence;
}
HamugaGeofenceOptions _buildGeofenceOptions(
HamugaGeofenceCircle officeGeofence,
) {
final onlyDuringWorkWindows = _readAnyConfigBool(const [
'GEOFENCE_ONLY_DURING_WORK_WINDOWS',
'GEOfENCE_ONLY_DURING_WORK_WINDOWS',
'GEOFENCE_ENABLE_TIME_WINDOW',
'GEOfENCE_ENABLE_TIME_WINDOW',
], defaultValue: false);
final backgroundMonitoringEnabled = _readAnyConfigBool(const [
'GEOFENCE_BACKGROUND_MONITORING',
'GEOfENCE_BACKGROUND_MONITORING',
], defaultValue: false);
return HamugaGeofenceOptions(
officeGeofence: officeGeofence,
backgroundMonitoring: HamugaGeofenceBackgroundMonitoringOptions(
enabled: backgroundMonitoringEnabled,
),
onlyDuringWorkWindows: onlyDuringWorkWindows,
onTransition: (transition, state) {
debugPrint(
'[Example] geofence ${transition.name}: ${state.status.name}',
);
},
onEvent: (event, _) {
debugPrint(
'[Example] background-aware event ${event.type.name}: ${event.geofenceId}',
);
},
);
}
Widget _buildLoadingState() {
return Stack(
children: [
// Placeholder background to give some context
Container(color: _uiTheme.colors.zinc100),
Center(
child: HamugaGlassPanel(
theme: _uiTheme,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(
_uiTheme.colors.primary,
),
),
),
const SizedBox(height: 20),
Text(
'Уншиж байна...',
style: _uiTheme
.suggestionTitleTextStyle(context)
.copyWith(fontSize: 16, letterSpacing: -0.2),
),
const SizedBox(height: 4),
Text(
'Газрын зургийн загвар ачаалж байна',
style: _uiTheme.suggestionSubtitleTextStyle(context),
),
],
),
),
),
),
],
);
}
Widget _buildErrorState() {
return Stack(
children: [
Container(color: _uiTheme.colors.backgroundLight),
Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: HamugaGlassPanel(
theme: _uiTheme,
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.red.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.error_outline,
size: 40,
color: Colors.red,
),
),
const SizedBox(height: 24),
Text(
'Алдаа гарлаа',
style: _uiTheme
.suggestionTitleTextStyle(context)
.copyWith(fontSize: 18),
),
const SizedBox(height: 8),
Text(
_error!,
textAlign: TextAlign.center,
style: _uiTheme
.suggestionSubtitleTextStyle(context)
.copyWith(fontSize: 14),
),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: () {
setState(() {
_error = null;
});
_initTileProxy();
},
style: ElevatedButton.styleFrom(
backgroundColor: _uiTheme.colors.primary,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: _uiTheme.radii.panel,
),
),
child: const Text(
'Дахин оролдох',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
],
),
),
),
),
),
],
);
}
}