ola_maps 0.3.2 copy "ola_maps: ^0.3.2" to clipboard
ola_maps: ^0.3.2 copied to clipboard

Ola Maps Flutter plugin (Android, iOS, and Web SDK map views) plus Places / Geocoder helpers. Vendored from ola_maps_flutter 0.2.0.

example/lib/main.dart

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:ola_maps/ola_maps.dart';
import 'package:permission_handler/permission_handler.dart';

import 'demo_data.dart';

const String kOlaMapsApiKey = String.fromEnvironment(
  'OLA_MAPS_API_KEY',
  defaultValue: 'YOUR_API_KEY',
);

const String kOlaMapsProjectId = String.fromEnvironment(
  'OLA_MAPS_PROJECT_ID',
  defaultValue: '',
);

const String kOlaMapsTileUrl = String.fromEnvironment(
  'OLA_MAPS_TILE_URL',
  defaultValue: kOlaMapsDefaultTileUrl,
);

const String kOlaMapsLanguage = String.fromEnvironment(
  'OLA_MAPS_LANGUAGE',
  defaultValue: 'en',
);

enum PinRole { drop, origin, destination }

void main() {
  if (isOlaMapsApiKeyConfigured(kOlaMapsApiKey)) {
    Olamaps.instance.initialize(
      kOlaMapsApiKey,
      language: kOlaMapsLanguage,
    );
  }
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Ola Maps Example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF222222),
          brightness: Brightness.light,
        ),
        useMaterial3: true,
        appBarTheme: const AppBarTheme(
          backgroundColor: Color(0xFF111111),
          foregroundColor: Colors.white,
        ),
      ),
      home: const OlaMapsDemoPage(),
    );
  }
}

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

  @override
  State<OlaMapsDemoPage> createState() => _OlaMapsDemoPageState();
}

class _OlaMapsDemoPageState extends State<OlaMapsDemoPage> {
  OlaMapController? _controller;
  late final OlaRoutingService _routingService;
  final _searchController = TextEditingController();
  final _searchFocus = FocusNode();
  Timer? _searchDebounce;
  List<AutoCompleteResults> _suggestions = [];
  bool _applyingSearch = false;
  OlaLatLng _cameraCenter = OlaMapsDemoData.mapCenter;

  String _status = isOlaMapsApiKeyConfigured(kOlaMapsApiKey)
      ? 'Loading Pune demo scene…'
      : 'Pass --dart-define=OLA_MAPS_API_KEY=YOUR_KEY';

  DemoPlace? _selectedPlace;
  OlaLatLng? _lastTap;
  String? _pointerAddress;
  bool _pointerMode = true;
  bool _searching = false;

  PinRole _pinRole = PinRole.drop;
  double _coverageRadius = OlaMapsDemoData.coverageRadiusMeters;
  double _circleRadius = 600;
  String _routeMode = 'driving';
  bool _routeAlternatives = false;

  OlaLatLng _origin = OlaMapsDemoData.pickup.position;
  OlaLatLng _destination = OlaMapsDemoData.drop.position;
  String _originLabel = OlaMapsDemoData.pickup.title;
  String _destinationLabel = OlaMapsDemoData.drop.title;

  final List<String> _placeMarkerIds = [];
  String? _zoneId;
  String? _coverageId;
  String? _circleId;
  String? _tripLineId;
  String? _curveId;
  String? _clusterId;
  String? _routeId;
  String? _droppedPinId;
  String? _originMarkerId;
  String? _destMarkerId;

  Timer? _reverseGeocodeDebounce;

  bool get _mapReady => _controller != null;

  @override
  void initState() {
    super.initState();
    _routingService = OlaRoutingService(apiKey: kOlaMapsApiKey);
    _searchController.addListener(_onSearchTextChanged);
    if (!kIsWeb) {
      Permission.location.request();
    }
  }

  @override
  void dispose() {
    _reverseGeocodeDebounce?.cancel();
    _searchDebounce?.cancel();
    _searchController.removeListener(_onSearchTextChanged);
    _searchController.dispose();
    _searchFocus.dispose();
    super.dispose();
  }

  void _setStatus(String message) {
    if (!mounted) return;
    setState(() => _status = message);
    debugPrint(message);
  }

  Future<void> _onMapReady(OlaMapController controller) async {
    setState(() => _controller = controller);
    controller.onMapClick = _onMapTap;
    controller.onCameraIdle = _onCameraIdle;
    controller.onMarkerClick = (id) {
      DemoPlace? place;
      for (final item in OlaMapsDemoData.places) {
        if (item.id == id) {
          place = item;
          break;
        }
      }
      setState(() => _selectedPlace = place);
      controller.showInfoWindow(id);
      _setStatus(place?.title ?? 'Marker $id');
    };
    await _showPlaces();
    await _refreshEndpointMarkers();
    _setStatus('Move the pointer or search an address');
  }

  void _onCameraIdle(OlaLatLng position) {
    _cameraCenter = position;
    if (!_pointerMode) return;
    _reverseGeocodeDebounce?.cancel();
    _reverseGeocodeDebounce = Timer(const Duration(milliseconds: 450), () {
      _reverseGeocode(position, updatePointer: true);
    });
  }

  Future<void> _onMapTap(OlaLatLng position) async {
    setState(() {
      _lastTap = position;
      _pointerMode = false;
    });
    final address = await _reverseGeocode(position);
    await _applyPinnedLocation(position, address ?? 'Dropped pin');
  }

  Future<String?> _reverseGeocode(
    OlaLatLng position, {
    bool updatePointer = false,
  }) async {
    if (!isOlaMapsApiKeyConfigured(kOlaMapsApiKey)) return null;
    try {
      final results = await Olamaps.instance.geoencoder.fetchAddresses(
        Location(lat: position.latitude, lng: position.longitude),
      );
      final address = results.isEmpty
          ? '${position.latitude.toStringAsFixed(5)}, ${position.longitude.toStringAsFixed(5)}'
          : results.first.formattedAddress.isNotEmpty
              ? results.first.formattedAddress
              : results.first.name;
      if (!mounted) return address;
      setState(() {
        if (updatePointer) _pointerAddress = address;
        _status = address;
      });
      return address;
    } catch (e) {
      _setStatus('Reverse geocode failed: $e');
      return null;
    }
  }

  void _onSearchTextChanged() {
    if (_applyingSearch) return;
    _searchDebounce?.cancel();
    final query = _searchController.text.trim();
    if (query.length < 2) {
      setState(() => _suggestions = []);
      return;
    }
    setState(() {});
    _searchDebounce = Timer(const Duration(milliseconds: 280), () {
      _fetchAutocomplete(query);
    });
  }

  Future<void> _fetchAutocomplete(String query) async {
    if (!isOlaMapsApiKeyConfigured(kOlaMapsApiKey)) {
      return;
    }
    setState(() => _searching = true);
    try {
      final results = await Olamaps.instance.places.getAutocompleteSuggestions(
        input: query,
        location: Location(
          lat: _cameraCenter.latitude,
          lng: _cameraCenter.longitude,
        ),
        radius: 50000,
        language: kOlaMapsLanguage,
      );
      if (!mounted || _searchController.text.trim() != query) return;
      setState(() => _suggestions = results);
    } catch (e) {
      if (!mounted) return;
      setState(() => _suggestions = []);
      _setStatus('Search failed: $e');
    } finally {
      if (mounted) setState(() => _searching = false);
    }
  }

  Future<OlaLatLng?> _positionForSuggestion(AutoCompleteResults hit) async {
    if (hit.geometry.hasCoordinates) {
      return OlaLatLng(hit.geometry.lat, hit.geometry.lng);
    }
    if (hit.placeId.isNotEmpty) {
      try {
        final details = await Olamaps.instance.places.getPlaceDetails(
          placeId: hit.placeId,
          language: kOlaMapsLanguage,
        );
        final loc = details.geometry.location;
        if (loc.hasCoordinates) {
          return OlaLatLng(loc.lat, loc.lng);
        }
      } catch (_) {}
    }
    if (hit.description.isEmpty) return null;
    final results = await Olamaps.instance.geoencoder.fetchLocation(
      hit.description,
      language: kOlaMapsLanguage,
    );
    if (results.isEmpty) return null;
    final loc = results.first.geometry.location;
    return OlaLatLng(loc.lat, loc.lng);
  }

  Future<void> _selectSuggestion(AutoCompleteResults hit) async {
    _searchDebounce?.cancel();
    _applyingSearch = true;
    _searchController.value = TextEditingValue(
      text: hit.description,
      selection: TextSelection.collapsed(offset: hit.description.length),
    );
    _searchFocus.unfocus();
    setState(() {
      _suggestions = [];
      _searching = true;
      _pointerMode = false;
    });
    try {
      final position = await _positionForSuggestion(hit);
      if (position == null) {
        _setStatus('Could not locate "${hit.description}"');
        return;
      }
      await _controller?.zoomToLocation(position, 16);
      await _applyPinnedLocation(position, hit.description);
      _setStatus(hit.description);
    } catch (e) {
      _setStatus('Search failed: $e');
    } finally {
      _applyingSearch = false;
      if (mounted) setState(() => _searching = false);
    }
  }

  Future<void> _searchAddress() async {
    final query = _searchController.text.trim();
    if (query.isEmpty || _controller == null) return;
    if (_suggestions.isNotEmpty) {
      await _selectSuggestion(_suggestions.first);
      return;
    }
    setState(() => _searching = true);
    try {
      final results = await Olamaps.instance.geoencoder.fetchLocation(query);
      if (results.isEmpty) {
        _setStatus('No geocode results for "$query"');
        return;
      }
      final hit = results.first;
      final position = OlaLatLng(
        hit.geometry.location.lat,
        hit.geometry.location.lng,
      );
      final label = hit.formattedAddress.isNotEmpty
          ? hit.formattedAddress
          : hit.name;
      await _controller!.zoomToLocation(position, 16);
      await _applyPinnedLocation(position, label);
      _setStatus('Geocoded: $label');
    } catch (e) {
      _setStatus('Geocode failed: $e');
    } finally {
      if (mounted) setState(() => _searching = false);
    }
  }

  Future<void> _applyPinnedLocation(OlaLatLng position, String label) async {
    switch (_pinRole) {
      case PinRole.drop:
        if (_droppedPinId != null) {
          await _controller?.removeMarker(_droppedPinId!);
        }
        final id = await _controller?.addMarker(
          markerId: 'dropped_pin',
          position: position,
          snippet: label,
          subSnippet:
              '${position.latitude.toStringAsFixed(5)}, ${position.longitude.toStringAsFixed(5)}',
        );
        setState(() {
          _droppedPinId = id;
          _lastTap = position;
        });
      case PinRole.origin:
        setState(() {
          _origin = position;
          _originLabel = label;
        });
        await _refreshEndpointMarkers();
      case PinRole.destination:
        setState(() {
          _destination = position;
          _destinationLabel = label;
        });
        await _refreshEndpointMarkers();
    }
  }

  Future<void> _usePointerLocation() async {
    final camera = await _controller?.getCameraPosition();
    if (camera == null) return;
    final address = _pointerAddress ?? await _reverseGeocode(camera);
    await _applyPinnedLocation(camera, address ?? 'Pointer');
    _setStatus('Pinned from pointer · ${_pinRole.name}');
  }

  Future<void> _refreshEndpointMarkers() async {
    if (_controller == null) return;
    if (_originMarkerId != null) {
      await _controller!.removeMarker(_originMarkerId!);
    }
    if (_destMarkerId != null) {
      await _controller!.removeMarker(_destMarkerId!);
    }
    _originMarkerId = await _controller!.addMarker(
      markerId: 'route_origin',
      position: _origin,
      snippet: 'Origin',
      subSnippet: _originLabel,
    );
    _destMarkerId = await _controller!.addMarker(
      markerId: 'route_dest',
      position: _destination,
      snippet: 'Destination',
      subSnippet: _destinationLabel,
    );
    setState(() {});
  }

  Future<void> _showPlaces() async {
    if (_controller == null) return;
    await _clearPlaceMarkers();
    for (final place in OlaMapsDemoData.places) {
      final id = await _controller!.addMarker(
        markerId: place.id,
        position: place.position,
        snippet: place.title,
        subSnippet: place.subtitle,
        isClickable: true,
      );
      if (id != null) _placeMarkerIds.add(id);
    }
    await _controller!.zoomToLocation(
      OlaMapsDemoData.mapCenter,
      OlaMapsDemoData.mapZoom,
    );
    setState(() {});
  }

  Future<void> _clearPlaceMarkers() async {
    for (final id in _placeMarkerIds) {
      await _controller?.removeMarker(id);
    }
    _placeMarkerIds.clear();
  }

  Future<void> _toggleDeliveryZone() async {
    if (_controller == null) return;
    if (_zoneId != null) {
      await _controller!.removePolygon(_zoneId!);
      setState(() => _zoneId = null);
      return;
    }
    final id = await _controller!.addPolygon(
      polygonId: 'magarpatta_zone',
      points: OlaMapsDemoData.deliveryZone,
      color: '#4CAF50',
      borderColor: '#1B8F3A',
      borderWidth: 3,
    );
    setState(() => _zoneId = id);
    await _controller!.zoomToLocation(
      OlaMapsDemoData.headquarters.position,
      14.5,
    );
  }

  Future<void> _toggleCoverage({bool forceOn = false}) async {
    if (_controller == null) return;
    if (_coverageId != null && !forceOn) {
      await _controller!.removeCircle(_coverageId!);
      setState(() => _coverageId = null);
      _setStatus('Coverage hidden');
      return;
    }
    if (_coverageId != null) {
      await _controller!.updateCircle(
        circleId: _coverageId!,
        radius: _coverageRadius,
      );
      _setStatus('Coverage ${_coverageRadius.round()} m');
      return;
    }
    final center = await _controller!.getCameraPosition() ??
        OlaMapsDemoData.coverageCenter;
    final id = await _controller!.addCircle(
      circleId: 'hq_coverage',
      center: center,
      radius: _coverageRadius,
      color: '#2196F3',
      opacity: 0.25,
      borderColor: '#1565C0',
      borderWidth: 2,
    );
    setState(() => _coverageId = id);
    _setStatus('Coverage ${_coverageRadius.round()} m');
  }

  Future<void> _toggleCircle({bool forceOn = false}) async {
    if (_controller == null) return;
    if (_circleId != null && !forceOn) {
      await _controller!.removeCircle(_circleId!);
      setState(() => _circleId = null);
      _setStatus('Geofence hidden');
      return;
    }
    final center = _lastTap ??
        await _controller!.getCameraPosition() ??
        OlaMapsDemoData.headquarters.position;
    if (_circleId != null) {
      await _controller!.updateCircle(
        circleId: _circleId!,
        center: center,
        radius: _circleRadius,
      );
      _setStatus('Geofence ${_circleRadius.round()} m');
      return;
    }
    final id = await _controller!.addCircle(
      circleId: 'pointer_geofence',
      center: center,
      radius: _circleRadius,
      color: '#FF9800',
      opacity: 0.22,
      borderColor: '#E65100',
      borderWidth: 2,
    );
    setState(() => _circleId = id);
    _setStatus('Geofence ${_circleRadius.round()} m');
  }

  Future<void> _toggleTripLine() async {
    if (_controller == null) return;
    if (_tripLineId != null) {
      await _controller!.removePolyline(_tripLineId!);
      setState(() => _tripLineId = null);
      return;
    }
    final id = await _controller!.addPolyline(
      polylineId: 'hq_to_mall',
      points: OlaMapsDemoData.sampleTrip,
      color: '#111111',
      width: 6,
      lineType: OlaLineType.solid,
    );
    setState(() => _tripLineId = id);
  }

  Future<void> _toggleBezier() async {
    if (_controller == null) return;
    if (_curveId != null) {
      await _controller!.removeBezierCurve(_curveId!);
      setState(() => _curveId = null);
      return;
    }
    final id = await _controller!.addBezierCurve(
      curveId: 'pickup_to_drop',
      startPoint: _origin,
      endPoint: _destination,
      color: '#E53935',
      width: 4,
      lineType: OlaLineType.solid,
    );
    setState(() => _curveId = id);
  }

  Future<void> _toggleClusters() async {
    if (_controller == null) return;
    if (_clusterId != null) {
      await _controller!.removeClusteredMarkers(_clusterId!);
      setState(() => _clusterId = null);
      return;
    }
    final id = await _controller!.addClusteredMarkersFromPoints(
      points: OlaMapsDemoData.nearbyStores,
      clusterRadius: 60,
      defaultMarkerColor: '#111111',
      defaultClusterColor: '#FBC02D',
      textColor: '#111111',
      textSize: 13,
    );
    setState(() => _clusterId = id);
  }

  Future<void> _drawLiveRoute() async {
    if (_controller == null) return;
    _setStatus('Fetching $_routeMode directions…');
    try {
      final routePoints = await _routingService.getDirections(
        originLat: _origin.latitude,
        originLng: _origin.longitude,
        destLat: _destination.latitude,
        destLng: _destination.longitude,
        mode: _routeMode,
        alternatives: _routeAlternatives,
      );
      final olaPoints = routePoints
          .map((point) => OlaLatLng(point['lat']!, point['lng']!))
          .toList();
      if (_routeId != null) {
        await _controller!.removePolyline(_routeId!);
      }
      final id = await _controller!.addPolyline(
        polylineId: 'live_route',
        points: olaPoints,
        color: '#1565C0',
        width: 7,
        lineType: OlaLineType.solid,
      );
      await _refreshEndpointMarkers();
      await _controller!.zoomToLocation(
        OlaLatLng(
          (_origin.latitude + _destination.latitude) / 2,
          (_origin.longitude + _destination.longitude) / 2,
        ),
        12.5,
      );
      setState(() => _routeId = id);
      _setStatus(
        '$_routeMode · $_originLabel → $_destinationLabel (${olaPoints.length} pts)',
      );
    } catch (e) {
      _setStatus('Route error: $e');
    }
  }

  Future<void> _goToMyLocation() async {
    await _controller?.showCurrentLocation();
    final location = await _controller?.getCurrentLocation();
    if (location == null) {
      _setStatus('Location unavailable');
      return;
    }
    await _controller!.zoomToLocation(location, 16);
    final address = await _reverseGeocode(location, updatePointer: true);
    _setStatus(address ?? 'Your location');
  }

  Future<void> _resetScene() async {
    if (_controller == null) return;
    if (_zoneId != null) await _controller!.removePolygon(_zoneId!);
    if (_coverageId != null) await _controller!.removeCircle(_coverageId!);
    if (_circleId != null) await _controller!.removeCircle(_circleId!);
    if (_tripLineId != null) await _controller!.removePolyline(_tripLineId!);
    if (_curveId != null) await _controller!.removeBezierCurve(_curveId!);
    if (_clusterId != null) {
      await _controller!.removeClusteredMarkers(_clusterId!);
    }
    if (_routeId != null) await _controller!.removePolyline(_routeId!);
    if (_droppedPinId != null) await _controller!.removeMarker(_droppedPinId!);
    setState(() {
      _zoneId = null;
      _coverageId = null;
      _circleId = null;
      _tripLineId = null;
      _curveId = null;
      _clusterId = null;
      _routeId = null;
      _droppedPinId = null;
      _selectedPlace = null;
      _lastTap = null;
      _origin = OlaMapsDemoData.pickup.position;
      _destination = OlaMapsDemoData.drop.position;
      _originLabel = OlaMapsDemoData.pickup.title;
      _destinationLabel = OlaMapsDemoData.drop.title;
    });
    await _showPlaces();
    await _refreshEndpointMarkers();
  }

  Future<void> _openSettings() async {
    await showModalBottomSheet<void>(
      context: context,
      isScrollControlled: true,
      showDragHandle: true,
      builder: (context) {
        return StatefulBuilder(
          builder: (context, setSheetState) {
            void update(VoidCallback fn) {
              setSheetState(fn);
              setState(fn);
            }

            return Padding(
              padding: EdgeInsets.fromLTRB(
                20,
                4,
                20,
                20 + MediaQuery.viewInsetsOf(context).bottom,
              ),
              child: SingleChildScrollView(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text(
                      'Map settings',
                      style: TextStyle(
                        fontSize: 20,
                        fontWeight: FontWeight.w700,
                      ),
                    ),
                    const SizedBox(height: 16),
                    const Text('Pin action'),
                    const SizedBox(height: 8),
                    Wrap(
                      spacing: 8,
                      children: [
                        for (final role in PinRole.values)
                          ChoiceChip(
                            label: Text(switch (role) {
                              PinRole.drop => 'Drop pin',
                              PinRole.origin => 'Set origin',
                              PinRole.destination => 'Set destination',
                            }),
                            selected: _pinRole == role,
                            onSelected: (_) =>
                                update(() => _pinRole = role),
                          ),
                      ],
                    ),
                    SwitchListTile(
                      contentPadding: EdgeInsets.zero,
                      title: const Text('Center pointer'),
                      subtitle: const Text(
                        'Reverse-geocode the map center as you pan',
                      ),
                      value: _pointerMode,
                      onChanged: (value) =>
                          update(() => _pointerMode = value),
                    ),
                    const Divider(),
                    Text('Coverage radius  ${_coverageRadius.round()} m'),
                    Slider(
                      min: 200,
                      max: 5000,
                      divisions: 24,
                      value: _coverageRadius,
                      label: '${_coverageRadius.round()} m',
                      onChanged: (value) =>
                          update(() => _coverageRadius = value),
                      onChangeEnd: (_) {
                        if (_coverageId != null) {
                          _toggleCoverage(forceOn: true);
                        }
                      },
                    ),
                    Text('Circle / geofence radius  ${_circleRadius.round()} m'),
                    Slider(
                      min: 100,
                      max: 3000,
                      divisions: 29,
                      value: _circleRadius,
                      label: '${_circleRadius.round()} m',
                      onChanged: (value) =>
                          update(() => _circleRadius = value),
                      onChangeEnd: (_) {
                        if (_circleId != null) {
                          _toggleCircle(forceOn: true);
                        }
                      },
                    ),
                    const Divider(),
                    const Text('Directions'),
                    const SizedBox(height: 8),
                    Text(
                      'From  $_originLabel',
                      style: TextStyle(color: Colors.grey.shade700),
                    ),
                    Text(
                      'To  $_destinationLabel',
                      style: TextStyle(color: Colors.grey.shade700),
                    ),
                    const SizedBox(height: 8),
                    DropdownButtonFormField<String>(
                      initialValue: _routeMode,
                      decoration: const InputDecoration(
                        labelText: 'Travel mode',
                        border: OutlineInputBorder(),
                      ),
                      items: const [
                        DropdownMenuItem(
                          value: 'driving',
                          child: Text('Driving'),
                        ),
                        DropdownMenuItem(
                          value: 'bicycling',
                          child: Text('Bicycling'),
                        ),
                        DropdownMenuItem(
                          value: 'walking',
                          child: Text('Walking'),
                        ),
                      ],
                      onChanged: (value) {
                        if (value != null) {
                          update(() => _routeMode = value);
                        }
                      },
                    ),
                    SwitchListTile(
                      contentPadding: EdgeInsets.zero,
                      title: const Text('Request alternatives'),
                      value: _routeAlternatives,
                      onChanged: (value) =>
                          update(() => _routeAlternatives = value),
                    ),
                    const SizedBox(height: 8),
                    FilledButton.icon(
                      onPressed: () {
                        Navigator.pop(context);
                        _drawLiveRoute();
                      },
                      icon: const Icon(Icons.navigation),
                      label: const Text('Draw route with these settings'),
                    ),
                    const SizedBox(height: 12),
                  ],
                ),
              ),
            );
          },
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Ola Maps Example'),
        actions: [
          IconButton(
            tooltip: 'Settings',
            onPressed: _openSettings,
            icon: const Icon(Icons.tune),
          ),
          IconButton(
            tooltip: 'Reset scene',
            onPressed: _mapReady ? _resetScene : null,
            icon: const Icon(Icons.refresh),
          ),
        ],
      ),
      body: Stack(
        children: [
          OlaMapView(
            apiKey: kOlaMapsApiKey,
            tileUrl: kOlaMapsTileUrl,
            language: kOlaMapsLanguage,
            projectId: kOlaMapsProjectId,
            initialCameraPosition: OlaMapsDemoData.mapCenter,
            initialZoom: OlaMapsDemoData.mapZoom,
            onMapError: _setStatus,
            onControllerReady: _onMapReady,
          ),
          if (_pointerMode)
            const IgnorePointer(
              child: Center(
                child: Padding(
                  padding: EdgeInsets.only(bottom: 28),
                  child: Icon(
                    Icons.location_on,
                    size: 40,
                    color: Color(0xFFE53935),
                  ),
                ),
              ),
            ),
          Positioned(
            top: 12,
            left: 12,
            right: 12,
            child: Column(
              children: [
                Material(
                  elevation: 3,
                  borderRadius: BorderRadius.circular(14),
                  color: Colors.white,
                  clipBehavior: Clip.antiAlias,
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      Padding(
                        padding: const EdgeInsets.fromLTRB(8, 4, 4, 4),
                        child: Row(
                          children: [
                            const SizedBox(width: 8),
                            const Icon(Icons.search, color: Colors.black54),
                            const SizedBox(width: 8),
                            Expanded(
                              child: TextField(
                                controller: _searchController,
                                focusNode: _searchFocus,
                                textInputAction: TextInputAction.search,
                                enabled: isOlaMapsApiKeyConfigured(kOlaMapsApiKey),
                                decoration: const InputDecoration(
                                  hintText: 'Search a location…',
                                  border: InputBorder.none,
                                ),
                                onSubmitted: (_) => _searchAddress(),
                              ),
                            ),
                            if (_searchController.text.isNotEmpty)
                              IconButton(
                                tooltip: 'Clear',
                                onPressed: () {
                                  _searchDebounce?.cancel();
                                  _searchController.clear();
                                  setState(() => _suggestions = []);
                                },
                                icon: const Icon(Icons.close, size: 20),
                              ),
                            if (_searching)
                              const Padding(
                                padding: EdgeInsets.all(12),
                                child: SizedBox(
                                  width: 18,
                                  height: 18,
                                  child: CircularProgressIndicator(strokeWidth: 2),
                                ),
                              )
                            else
                              IconButton(
                                onPressed: _mapReady ? _searchAddress : null,
                                icon: const Icon(Icons.arrow_forward),
                              ),
                          ],
                        ),
                      ),
                      if (_suggestions.isNotEmpty)
                        ConstrainedBox(
                          constraints: const BoxConstraints(maxHeight: 280),
                          child: ListView.separated(
                            shrinkWrap: true,
                            padding: EdgeInsets.zero,
                            itemCount: _suggestions.length,
                            separatorBuilder: (_, __) => const Divider(height: 1),
                            itemBuilder: (context, index) {
                              final hit = _suggestions[index];
                              final subtitle =
                                  hit.structuredFormatting.secondaryText;
                              return ListTile(
                                dense: true,
                                leading: const Icon(Icons.place_outlined),
                                title: Text(
                                  hit.structuredFormatting.mainText.isNotEmpty
                                      ? hit.structuredFormatting.mainText
                                      : hit.description,
                                ),
                                subtitle: subtitle.isNotEmpty
                                    ? Text(
                                        subtitle,
                                        maxLines: 1,
                                        overflow: TextOverflow.ellipsis,
                                      )
                                    : hit.description !=
                                            hit.structuredFormatting.mainText
                                        ? Text(
                                            hit.description,
                                            maxLines: 1,
                                            overflow: TextOverflow.ellipsis,
                                          )
                                        : null,
                                trailing: hit.distanceMeters == null
                                    ? null
                                    : Text(
                                        hit.distanceMeters! >= 1000
                                            ? '${(hit.distanceMeters! / 1000).toStringAsFixed(1)} km'
                                            : '${hit.distanceMeters} m',
                                        style: Theme.of(context)
                                            .textTheme
                                            .bodySmall,
                                      ),
                                onTap: () => _selectSuggestion(hit),
                              );
                            },
                          ),
                        ),
                    ],
                  ),
                ),
                const SizedBox(height: 8),
                _StatusCard(
                  status: _status,
                  selectedPlace: _selectedPlace,
                  lastTap: _lastTap,
                  pointerAddress: _pointerAddress,
                  originLabel: _originLabel,
                  destinationLabel: _destinationLabel,
                  pinRole: _pinRole,
                ),
              ],
            ),
          ),
          Positioned(
            right: 12,
            bottom: 12,
            child: Column(
              children: [
                _RoundButton(
                  icon: Icons.add,
                  onPressed: _mapReady ? () => _controller!.zoomIn() : null,
                ),
                const SizedBox(height: 8),
                _RoundButton(
                  icon: Icons.remove,
                  onPressed: _mapReady ? () => _controller!.zoomOut() : null,
                ),
                const SizedBox(height: 8),
                _RoundButton(
                  icon: Icons.my_location,
                  onPressed: _mapReady ? _goToMyLocation : null,
                ),
                const SizedBox(height: 8),
                _RoundButton(
                  icon: Icons.push_pin,
                  onPressed: _mapReady && _pointerMode
                      ? _usePointerLocation
                      : null,
                ),
              ],
            ),
          ),
        ],
      ),
      bottomNavigationBar: Material(
        elevation: 10,
        color: Colors.white,
        child: SafeArea(
          child: SingleChildScrollView(
            scrollDirection: Axis.horizontal,
            padding: const EdgeInsets.fromLTRB(10, 10, 10, 8),
            child: Row(
              children: [
                _LayerChip(
                  label: 'Places',
                  icon: Icons.place,
                  selected: _placeMarkerIds.isNotEmpty,
                  onPressed: _mapReady ? _showPlaces : null,
                ),
                _LayerChip(
                  label: 'Zone',
                  icon: Icons.hexagon_outlined,
                  selected: _zoneId != null,
                  onPressed: _mapReady ? _toggleDeliveryZone : null,
                ),
                _LayerChip(
                  label: 'Coverage',
                  icon: Icons.radar,
                  selected: _coverageId != null,
                  onPressed: _mapReady ? _toggleCoverage : null,
                ),
                _LayerChip(
                  label: 'Circle',
                  icon: Icons.circle_outlined,
                  selected: _circleId != null,
                  onPressed: _mapReady ? _toggleCircle : null,
                ),
                _LayerChip(
                  label: 'Trip',
                  icon: Icons.alt_route,
                  selected: _tripLineId != null,
                  onPressed: _mapReady ? _toggleTripLine : null,
                ),
                _LayerChip(
                  label: 'Curve',
                  icon: Icons.trending_flat,
                  selected: _curveId != null,
                  onPressed: _mapReady ? _toggleBezier : null,
                ),
                _LayerChip(
                  label: 'Stores',
                  icon: Icons.bubble_chart,
                  selected: _clusterId != null,
                  onPressed: _mapReady ? _toggleClusters : null,
                ),
                _LayerChip(
                  label: 'Directions',
                  icon: Icons.navigation,
                  selected: _routeId != null,
                  onPressed: _mapReady ? _drawLiveRoute : null,
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class _StatusCard extends StatelessWidget {
  final String status;
  final DemoPlace? selectedPlace;
  final OlaLatLng? lastTap;
  final String? pointerAddress;
  final String originLabel;
  final String destinationLabel;
  final PinRole pinRole;

  const _StatusCard({
    required this.status,
    required this.selectedPlace,
    required this.lastTap,
    required this.pointerAddress,
    required this.originLabel,
    required this.destinationLabel,
    required this.pinRole,
  });

  @override
  Widget build(BuildContext context) {
    return Material(
      elevation: 3,
      borderRadius: BorderRadius.circular(14),
      color: Colors.white,
      child: Padding(
        padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              selectedPlace?.title ?? 'Ola Maps',
              style: const TextStyle(
                fontWeight: FontWeight.w700,
                fontSize: 16,
              ),
            ),
            const SizedBox(height: 4),
            Text(
              pointerAddress ?? selectedPlace?.subtitle ?? status,
              style: TextStyle(color: Colors.grey.shade700, height: 1.3),
            ),
            const SizedBox(height: 8),
            Text(
              'Pin → ${pinRole.name}   ·   $originLabel → $destinationLabel',
              style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
            ),
            if (lastTap != null) ...[
              const SizedBox(height: 4),
              Text(
                'Last tap  ${lastTap!.latitude.toStringAsFixed(5)}, ${lastTap!.longitude.toStringAsFixed(5)}',
                style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
              ),
            ],
          ],
        ),
      ),
    );
  }
}

class _LayerChip extends StatelessWidget {
  final String label;
  final IconData icon;
  final bool selected;
  final VoidCallback? onPressed;

  const _LayerChip({
    required this.label,
    required this.icon,
    required this.selected,
    this.onPressed,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 4),
      child: FilterChip(
        selected: selected,
        avatar: Icon(icon, size: 18),
        label: Text(label),
        onSelected: onPressed == null ? null : (_) => onPressed!(),
        selectedColor: const Color(0xFFFBC02D),
        checkmarkColor: Colors.black,
      ),
    );
  }
}

class _RoundButton extends StatelessWidget {
  final IconData icon;
  final VoidCallback? onPressed;

  const _RoundButton({required this.icon, this.onPressed});

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton.small(
      heroTag: icon.codePoint.toString(),
      onPressed: onPressed,
      backgroundColor: Colors.white,
      foregroundColor: Colors.black87,
      child: Icon(icon),
    );
  }
}
4
likes
0
points
326
downloads

Publisher

unverified uploader

Weekly Downloads

Ola Maps Flutter plugin (Android, iOS, and Web SDK map views) plus Places / Geocoder helpers. Vendored from ola_maps_flutter 0.2.0.

Repository (GitHub)
View/report issues

Topics

#maps #ola-maps #navigation #geolocation #markers

License

unknown (license)

Dependencies

animated_custom_dropdown, flutter, flutter_web_plugins, http, plugin_platform_interface, web

More

Packages that depend on ola_maps

Packages that implement ola_maps