hamuga_imap_sdk 0.0.13 copy "hamuga_imap_sdk: ^0.0.13" to clipboard
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),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
        ),
      ],
    );
  }
}
0
likes
140
points
209
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

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.

Repository

Topics

#hamuga-imap #mongolian-map #map-sdk #maplibre #vector-tiles

License

Apache-2.0 (license)

Dependencies

dio, flutter, flutter_svg, maplibre_gl, permission_handler, shared_preferences, shelf

More

Packages that depend on hamuga_imap_sdk

Packages that implement hamuga_imap_sdk