mhj_maps 1.2.5 copy "mhj_maps: ^1.2.5" to clipboard
mhj_maps: ^1.2.5 copied to clipboard

The open-source Maps SDK for Flutter. Zero API keys, zero cost. High-performance routing, geocoding, autocomplete, and fully customizable map rendering powered by OpenStreetMap.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:mhj_maps/mhj_maps.dart';
import 'dart:ui';

void main() {
  runApp(const MhjMapsExampleApp());
}

class MhjMapsExampleApp extends StatelessWidget {
  const MhjMapsExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'MhjMaps Example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF00FF94),
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
        fontFamily: 'Inter',
      ),
      home: const MapExampleScreen(),
    );
  }
}

class MapExampleScreen extends StatefulWidget {
  const MapExampleScreen({super.key});

  @override
  State<MapExampleScreen> createState() => _MapExampleScreenState();
}

class _MapExampleScreenState extends State<MapExampleScreen> {
  final MhjMaps _mhjMaps = MhjMaps();
  MhjMapsMapController? _mapController;

  final TextEditingController _originController = TextEditingController(
    text: 'Paris, France',
  );
  final TextEditingController _destController = TextEditingController(
    text: 'Lyon, France',
  );

  AutocompleteResult? _selectedOrigin;
  AutocompleteResult? _selectedDestination;

  bool _isLoading = false;
  RouteResult? _routeResult;

  // ─── Map Customization ────────────────────────────────────────────
  MhjMapsMapTheme _currentTheme = MhjMapsMapThemes.standard;
  bool _showThemePicker = false;

  Future<void> _calculateRoute() async {
    setState(() => _isLoading = true);
    try {
      final MhjMapsLatLng originCoords;
      if (_selectedOrigin != null &&
          _selectedOrigin!.displayName == _originController.text) {
        originCoords = MhjMapsLatLng(
          lat: _selectedOrigin!.lat,
          lng: _selectedOrigin!.lng,
        );
      } else {
        final origin = await _mhjMaps.geocode(_originController.text);
        originCoords = MhjMapsLatLng(lat: origin.lat, lng: origin.lng);
      }

      final MhjMapsLatLng destCoords;
      if (_selectedDestination != null &&
          _selectedDestination!.displayName == _destController.text) {
        destCoords = MhjMapsLatLng(
          lat: _selectedDestination!.lat,
          lng: _selectedDestination!.lng,
        );
      } else {
        final dest = await _mhjMaps.geocode(_destController.text);
        destCoords = MhjMapsLatLng(lat: dest.lat, lng: dest.lng);
      }

      final result = await _mhjMaps.route(
        origin: originCoords,
        destination: destCoords,
      );

      setState(() {
        _routeResult = result;
        _isLoading = false;
      });

      if (_mapController != null) {
        _mapController!.clearAll();

        _mapController!.addMarker(
          position: originCoords,
          icon: _buildSmartMarker(Colors.blue, Icons.my_location),
        );
        _mapController!.addMarker(
          position: destCoords,
          icon: _buildSmartMarker(
            _parseHexColor(_currentTheme.markerColor),
            Icons.flag,
          ),
        );

        _mapController!.drawRoute(
          result.polyline,
          color: _parseHexColor(_currentTheme.polylineColor),
          width: 5.0,
          borderColor: Colors.black26,
          borderWidth: 1,
        );

        _mapController!.fitRoute(result.polyline);
      }
    } catch (e) {
      setState(() => _isLoading = false);
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Error: ${e.toString()}')),
        );
      }
    }
  }

  Widget _buildSmartMarker(Color color, IconData icon) {
    return Container(
      width: 40,
      height: 40,
      decoration: BoxDecoration(
        color: color,
        shape: BoxShape.circle,
        border: Border.all(color: Colors.white, width: 2),
        boxShadow: [
          BoxShadow(
            color: color.withValues(alpha: 0.4),
            blurRadius: 12,
            spreadRadius: 2,
          ),
        ],
      ),
      child: Icon(icon, color: Colors.white, size: 20),
    );
  }

  Color _parseHexColor(String hex) {
    hex = hex.replaceAll('#', '');
    if (hex.length == 6) hex = 'FF$hex';
    return Color(int.parse(hex, radix: 16));
  }

  void _onThemeSelected(MhjMapsMapTheme theme) {
    setState(() {
      _currentTheme = theme;
      _showThemePicker = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          // ─── The Map (with theme) ─────────────────────────────────
          MhjMapsMap(
            key: ValueKey(_currentTheme.id),
            center: const MhjMapsLatLng(lat: 48.8566, lng: 2.3522),
            zoom: 12,
            theme: _currentTheme,
            showZoomControls: true,
            onMapCreated: (controller) {
              _mapController = controller;
              // Redraw route if we had one
              if (_routeResult != null) {
                _calculateRoute();
              }
            },
            onTap: (position) {
              debugPrint('Map tapped: $position');
            },
          ),

          // ─── Search UI Overlay ────────────────────────────────────
          SafeArea(
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16.0),
              child: Column(
                children: [
                  const SizedBox(height: 10),
                  _buildGlassContainer(
                    child: Column(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        _buildSearchInput(
                          controller: _originController,
                          hint: 'Origin',
                          icon: Icons.my_location,
                          iconColor: Colors.blue,
                          onOptionSelected: (s) => _selectedOrigin = s,
                        ),
                        const Divider(height: 1, color: Colors.white24),
                        _buildSearchInput(
                          controller: _destController,
                          hint: 'Destination',
                          icon: Icons.location_on,
                          iconColor: const Color(0xFF00FF94),
                          onOptionSelected: (s) => _selectedDestination = s,
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
          ),

          // ─── Theme Picker Button ──────────────────────────────────
          Positioned(
            top: MediaQuery.of(context).padding.top + 120,
            right: 16,
            child: _buildGlassButton(
              icon: Icons.palette_outlined,
              onPressed: () =>
                  setState(() => _showThemePicker = !_showThemePicker),
            ),
          ),

          // ─── Theme Picker Panel ───────────────────────────────────
          if (_showThemePicker)
            Positioned(
              top: MediaQuery.of(context).padding.top + 170,
              right: 16,
              child: _buildThemePicker(),
            ),

          // ─── Current Theme Badge ──────────────────────────────────
          Positioned(
            top: MediaQuery.of(context).padding.top + 120,
            left: 16,
            child: _buildGlassContainer(
              child: Padding(
                padding: const EdgeInsets.symmetric(
                  horizontal: 12,
                  vertical: 8,
                ),
                child: Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Container(
                      width: 10,
                      height: 10,
                      decoration: BoxDecoration(
                        color: _parseHexColor(_currentTheme.polylineColor),
                        shape: BoxShape.circle,
                      ),
                    ),
                    const SizedBox(width: 8),
                    Text(
                      _currentTheme.name,
                      style: const TextStyle(
                        color: Colors.white,
                        fontSize: 12,
                        fontWeight: FontWeight.w600,
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),

          // ─── Bottom Route Info ────────────────────────────────────
          if (_routeResult != null || _isLoading)
            Positioned(
              bottom: 40,
              left: 16,
              right: 16,
              child: _buildGlassContainer(
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: _isLoading
                      ? const Center(child: CircularProgressIndicator())
                      : Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Row(
                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
                              children: [
                                Column(
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  children: [
                                    const Text(
                                      'Estimated Travel',
                                      style: TextStyle(
                                        color: Colors.white70,
                                        fontSize: 12,
                                      ),
                                    ),
                                    Text(
                                      _routeResult!.durationText,
                                      style: const TextStyle(
                                        color: Colors.white,
                                        fontSize: 24,
                                        fontWeight: FontWeight.bold,
                                      ),
                                    ),
                                  ],
                                ),
                                Column(
                                  crossAxisAlignment: CrossAxisAlignment.end,
                                  children: [
                                    const Text(
                                      'Distance',
                                      style: TextStyle(
                                        color: Colors.white70,
                                        fontSize: 12,
                                      ),
                                    ),
                                    Text(
                                      _routeResult!.distanceText,
                                      style: TextStyle(
                                        color: _parseHexColor(
                                          _currentTheme.polylineColor,
                                        ),
                                        fontSize: 20,
                                        fontWeight: FontWeight.w600,
                                      ),
                                    ),
                                  ],
                                ),
                              ],
                            ),
                            const SizedBox(height: 16),
                            SizedBox(
                              width: double.infinity,
                              child: ElevatedButton(
                                onPressed: _calculateRoute,
                                style: ElevatedButton.styleFrom(
                                  backgroundColor: _parseHexColor(
                                    _currentTheme.polylineColor,
                                  ),
                                  foregroundColor:
                                      _currentTheme.isDark
                                          ? Colors.black
                                          : Colors.white,
                                  padding: const EdgeInsets.symmetric(
                                    vertical: 16,
                                  ),
                                  shape: RoundedRectangleBorder(
                                    borderRadius: BorderRadius.circular(12),
                                  ),
                                ),
                                child: const Text(
                                  'RECALCULATE ROUTE',
                                  style: TextStyle(fontWeight: FontWeight.bold),
                                ),
                              ),
                            ),
                          ],
                        ),
                ),
              ),
            )
          else
            Positioned(
              bottom: 40,
              right: 16,
              left: 16,
              child: SizedBox(
                height: 60,
                child: ElevatedButton(
                  onPressed: _calculateRoute,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: _parseHexColor(
                      _currentTheme.polylineColor,
                    ),
                    foregroundColor:
                        _currentTheme.isDark ? Colors.black : Colors.white,
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(16),
                    ),
                  ),
                  child: const Text(
                    'GET DIRECTIONS',
                    style: TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ),
            ),
        ],
      ),
    );
  }

  // ─── UI Builders ──────────────────────────────────────────────────

  Widget _buildThemePicker() {
    return _buildGlassContainer(
      child: SizedBox(
        width: 220,
        height: 340,
        child: ListView.builder(
          padding: const EdgeInsets.symmetric(vertical: 8),
          itemCount: MhjMapsMapThemes.all.length,
          itemBuilder: (context, index) {
            final theme = MhjMapsMapThemes.all[index];
            final isSelected = theme.id == _currentTheme.id;

            return InkWell(
              onTap: () => _onThemeSelected(theme),
              child: Container(
                padding: const EdgeInsets.symmetric(
                  horizontal: 14,
                  vertical: 10,
                ),
                color: isSelected
                    ? Colors.white.withValues(alpha: 0.1)
                    : Colors.transparent,
                child: Row(
                  children: [
                    Container(
                      width: 28,
                      height: 28,
                      decoration: BoxDecoration(
                        color: _parseHexColor(theme.polylineColor),
                        borderRadius: BorderRadius.circular(6),
                        border: isSelected
                            ? Border.all(color: Colors.white, width: 2)
                            : null,
                      ),
                      child: theme.isDark
                          ? const Icon(
                              Icons.dark_mode,
                              color: Colors.white70,
                              size: 14,
                            )
                          : const Icon(
                              Icons.light_mode,
                              color: Colors.white70,
                              size: 14,
                            ),
                    ),
                    const SizedBox(width: 10),
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            theme.name,
                            style: TextStyle(
                              color: Colors.white,
                              fontSize: 13,
                              fontWeight: isSelected
                                  ? FontWeight.bold
                                  : FontWeight.w500,
                            ),
                          ),
                          Text(
                            theme.description,
                            maxLines: 1,
                            overflow: TextOverflow.ellipsis,
                            style: const TextStyle(
                              color: Colors.white38,
                              fontSize: 10,
                            ),
                          ),
                        ],
                      ),
                    ),
                    if (isSelected)
                      const Icon(Icons.check, color: Colors.white, size: 16),
                  ],
                ),
              ),
            );
          },
        ),
      ),
    );
  }

  Widget _buildGlassButton({
    required IconData icon,
    required VoidCallback onPressed,
  }) {
    return GestureDetector(
      onTap: onPressed,
      child: Container(
        width: 44,
        height: 44,
        decoration: BoxDecoration(
          color: Colors.black.withValues(alpha: 0.6),
          borderRadius: BorderRadius.circular(12),
          border: Border.all(color: Colors.white.withValues(alpha: 0.1)),
        ),
        child: ClipRRect(
          borderRadius: BorderRadius.circular(12),
          child: BackdropFilter(
            filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
            child: Icon(icon, color: Colors.white, size: 20),
          ),
        ),
      ),
    );
  }

  Widget _buildGlassContainer({required Widget child}) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.black.withValues(alpha: 0.6),
        borderRadius: BorderRadius.circular(24),
        border: Border.all(color: Colors.white.withValues(alpha: 0.1)),
      ),
      child: ClipRRect(
        borderRadius: BorderRadius.circular(24),
        child: BackdropFilter(
          filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
          child: child,
        ),
      ),
    );
  }

  Widget _buildSearchInput({
    required TextEditingController controller,
    required String hint,
    required IconData icon,
    required Color iconColor,
    void Function(AutocompleteResult)? onOptionSelected,
  }) {
    return Autocomplete<AutocompleteResult>(
      displayStringForOption: (AutocompleteResult option) =>
          option.displayName,
      optionsBuilder: (TextEditingValue textEditingValue) async {
        if (textEditingValue.text.isEmpty) {
          return const Iterable<AutocompleteResult>.empty();
        }
        return await _mhjMaps.autocomplete(textEditingValue.text);
      },
      onSelected: (AutocompleteResult selection) {
        controller.text = selection.displayName;
        onOptionSelected?.call(selection);
        _calculateRoute();
      },
      fieldViewBuilder:
          (context, fieldController, focusNode, onFieldSubmitted) {
        if (fieldController.text != controller.text) {
          fieldController.text = controller.text;
        }

        fieldController.addListener(() {
          if (controller.text != fieldController.text) {
            controller.text = fieldController.text;
          }
        });

        return TextField(
          controller: fieldController,
          focusNode: focusNode,
          onSubmitted: (value) => onFieldSubmitted(),
          style: const TextStyle(color: Colors.white, fontSize: 14),
          decoration: InputDecoration(
            hintText: hint,
            hintStyle: const TextStyle(color: Colors.white38, fontSize: 14),
            prefixIcon: Icon(icon, color: iconColor, size: 20),
            border: InputBorder.none,
            contentPadding: const EdgeInsets.symmetric(
              vertical: 18,
              horizontal: 16,
            ),
            suffixIcon: fieldController.text.isNotEmpty
                ? IconButton(
                    icon: const Icon(
                      Icons.clear,
                      color: Colors.white38,
                      size: 18,
                    ),
                    onPressed: () {
                      fieldController.clear();
                      controller.clear();
                    },
                  )
                : null,
          ),
        );
      },
      optionsViewBuilder: (context, onSelected, options) {
        return Align(
          alignment: Alignment.topLeft,
          child: Material(
            color: Colors.transparent,
            child: Container(
              width: MediaQuery.of(context).size.width - 32,
              margin: const EdgeInsets.only(top: 4),
              decoration: BoxDecoration(
                color: const Color(0xFF1A1A1A).withValues(alpha: 0.95),
                borderRadius: BorderRadius.circular(16),
                border: Border.all(color: Colors.white12),
                boxShadow: [
                  BoxShadow(
                    color: Colors.black.withValues(alpha: 0.5),
                    blurRadius: 20,
                    offset: const Offset(0, 10),
                  ),
                ],
              ),
              child: ClipRRect(
                borderRadius: BorderRadius.circular(16),
                child: BackdropFilter(
                  filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
                  child: ListView.separated(
                    padding: EdgeInsets.zero,
                    shrinkWrap: true,
                    itemCount: options.length,
                    separatorBuilder: (context, index) =>
                        const Divider(height: 1, color: Colors.white10),
                    itemBuilder: (BuildContext context, int index) {
                      final AutocompleteResult option =
                          options.elementAt(index);
                      return ListTile(
                        leading: const Icon(
                          Icons.location_on_outlined,
                          color: Colors.white38,
                          size: 18,
                        ),
                        title: Text(
                          option.name,
                          style: const TextStyle(
                            color: Colors.white,
                            fontWeight: FontWeight.w600,
                            fontSize: 13,
                          ),
                        ),
                        subtitle: Text(
                          option.displayName,
                          maxLines: 1,
                          overflow: TextOverflow.ellipsis,
                          style: const TextStyle(
                            color: Colors.white38,
                            fontSize: 11,
                          ),
                        ),
                        onTap: () => onSelected(option),
                      );
                    },
                  ),
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}
1
likes
125
points
240
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

The open-source Maps SDK for Flutter. Zero API keys, zero cost. High-performance routing, geocoding, autocomplete, and fully customizable map rendering powered by OpenStreetMap.

Repository (GitHub)
View/report issues

Topics

#maps #openstreetmap #geocoding #routing #navigation

License

Apache-2.0 (license)

Dependencies

collection, flutter, flutter_map, flutter_svg, http, latlong2

More

Packages that depend on mhj_maps