smart_location 0.0.5 copy "smart_location: ^0.0.5" to clipboard
smart_location: ^0.0.5 copied to clipboard

Smart Location is a Flutter location plugin for GPS tracking, real-time location updates, geofencing, distance calculation.

example/lib/main.dart

import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:smart_location/smart_location.dart';

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

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

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

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

  @override
  State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateMixin {
  late TabController _tabController;
  LocationData? _currentLocation;
  bool _isLoading = false;
  String _errorMsg = '';
  StreamSubscription<LocationData>? _streamSubscription;

  // Privacy Guard states
  LocationData? _fuzzedLocation;
  LocationData? _snappedLocation;
  double _fuzzRadius = 500.0;
  double _gridSize = 0.01;

  // Route Recorder states
  bool _isRecording = false;
  bool _isPaused = false;
  Duration _recordElapsed = Duration.zero;
  Timer? _recordTimer;
  List<LocationData> _recordedPath = [];
  List<LocationData> _simplifiedPath = [];
  double _recordedDistance = 0.0;
  String _encodedPolyline = '';
  GeoBounds? _routeBounds;

  // Geofencing states
  final List<GeofenceEvent> _geofenceEvents = [];
  bool _isGeofenceMonitoring = false;
  double _geofenceRadius = 50.0;

  // Speed Monitoring states
  final List<String> _speedLogs = [];
  double _speedLimitKmh = 60.0;
  bool _isSpeedMonitoring = false;
  StreamSubscription<SpeedAlert>? _speedAlertSub;
  StreamSubscription<GForceAlert>? _gForceAlertSub;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 6, vsync: this);
    _initLocation();
  }

  Future<void> _initLocation() async {
    setState(() {
      _isLoading = true;
      _errorMsg = '';
    });
    try {
      await SmartLocation.ensureReady();
      final loc = await SmartLocation.current();
      setState(() {
        _currentLocation = loc;
        _isLoading = false;
      });
      _updatePrivacyStates(loc);
    } catch (e) {
      setState(() {
        _errorMsg = e.toString();
        _isLoading = false;
      });
    }
  }

  void _updatePrivacyStates(LocationData loc) {
    setState(() {
      _fuzzedLocation = PrivacyGuard.fuzz(loc, radiusInMeters: _fuzzRadius);
      _snappedLocation = PrivacyGuard.snapToGrid(loc, gridSizeDegrees: _gridSize);
    });
  }

  // Stream-based Live Updates
  void _toggleLiveStream(bool enable) {
    if (enable) {
      _streamSubscription = SmartLocation.stream.listen((loc) {
        setState(() {
          _currentLocation = loc;
        });
        _updatePrivacyStates(loc);
      }, onError: (err) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Stream error: $err')),
        );
      });
    } else {
      _streamSubscription?.cancel();
      _streamSubscription = null;
    }
    setState(() {});
  }

  // Route Recording triggers
  void _startRecording() {
    SmartLocation.recorder.start(SmartLocation.stream);
    _recordTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
      setState(() {
        _recordElapsed = SmartLocation.recorder.elapsedTime;
        _recordedDistance = SmartLocation.recorder.totalDistanceInMeters;
      });
    });
    setState(() {
      _isRecording = true;
      _isPaused = false;
      _simplifiedPath.clear();
      _recordedPath.clear();
      _encodedPolyline = '';
      _routeBounds = null;
    });
  }

  void _pauseRecording() {
    SmartLocation.recorder.pause();
    setState(() {
      _isPaused = true;
    });
  }

  void _resumeRecording() {
    SmartLocation.recorder.resume();
    setState(() {
      _isPaused = false;
    });
  }

  void _stopRecording() {
    _recordTimer?.cancel();
    final path = SmartLocation.recorder.stop();
    setState(() {
      _isRecording = false;
      _isPaused = false;
      _recordedPath = path;
      _recordElapsed = Duration.zero;
      _recordedDistance = 0.0;
      if (path.isNotEmpty) {
        _encodedPolyline = SmartLocation.maps.encodePolyline(path);
        _routeBounds = SmartLocation.maps.calculateBounds(path);
      }
    });
  }

  void _simplifyRecordedPath() {
    if (_recordedPath.isEmpty) return;
    final simplified = SmartLocation.simplifyPath(_recordedPath, 10.0); // 10m tolerance
    setState(() {
      _simplifiedPath = simplified;
      if (simplified.isNotEmpty) {
        _encodedPolyline = SmartLocation.maps.encodePolyline(simplified);
        _routeBounds = SmartLocation.maps.calculateBounds(simplified);
      }
    });
  }

  // Geofencing triggers
  void _setupGeofenceAndStart() {
    if (_currentLocation == null) return;
    
    SmartLocation.geofence.clearGeofences();
    _geofenceEvents.clear();

    final gf = Geofence(
      id: "demo_center",
      latitude: _currentLocation!.latitude,
      longitude: _currentLocation!.longitude,
      radiusInMeters: _geofenceRadius,
      triggerTypes: {GeofenceTrigger.enter, GeofenceTrigger.exit, GeofenceTrigger.dwell},
      dwellDuration: const Duration(seconds: 10),
    );

    SmartLocation.geofence.addGeofence(gf);
    SmartLocation.geofence.startMonitoring(SmartLocation.stream);

    SmartLocation.geofence.events.listen((event) {
      setState(() {
        _geofenceEvents.insert(0, event);
      });
    });

    setState(() {
      _isGeofenceMonitoring = true;
    });
  }

  void _stopGeofenceMonitoring() {
    SmartLocation.geofence.stopMonitoring();
    setState(() {
      _isGeofenceMonitoring = false;
    });
  }

  // Speed Monitoring triggers
  void _toggleSpeedMonitoring(bool enable) {
    if (enable) {
      _speedLogs.clear();
      SmartLocation.speed.configure(speedLimitKmh: _speedLimitKmh);
      SmartLocation.speed.startMonitoring(SmartLocation.stream);

      _speedAlertSub = SmartLocation.speed.speedAlerts.listen((event) {
        setState(() {
          _speedLogs.insert(0, '[SPEED LIMIT ALERT] ${event.message}');
        });
      });

      _gForceAlertSub = SmartLocation.speed.gForceAlerts.listen((event) {
        setState(() {
          _speedLogs.insert(0, '[BRAKE/ACCEL ALERT] ${event.message}');
        });
      });
    } else {
      SmartLocation.speed.stopMonitoring();
      _speedAlertSub?.cancel();
      _gForceAlertSub?.cancel();
    }
    setState(() {
      _isSpeedMonitoring = enable;
    });
  }

  // Simulates a driving route with acceleration and braking to demonstrate alerts in the UI
  void _simulateTripAlerts() {
    final streamController = StreamController<LocationData>();
    final simulatedSpeedMonitor = SpeedMonitor();
    simulatedSpeedMonitor.configure(speedLimitKmh: 50.0); // 50 km/h threshold

    final List<String> simulatedLogs = [];
    simulatedSpeedMonitor.speedAlerts.listen((e) => simulatedLogs.add('[SPEED WARNING] ${e.message}'));
    simulatedSpeedMonitor.gForceAlerts.listen((e) => simulatedLogs.add('[BRAKE/ACCEL] ${e.message}'));

    simulatedSpeedMonitor.startMonitoring(streamController.stream);

    final t1 = DateTime.now();
    final t2 = t1.add(const Duration(seconds: 1));
    final t3 = t1.add(const Duration(seconds: 2));

    // 1. Stable slow speed
    streamController.add(LocationData(latitude: 0, longitude: 0, accuracy: 1, altitude: 0, speed: 8.0, speedAccuracy: 0, heading: 0, timestamp: t1));
    
    // 2. High accel (8 m/s to 25 m/s = 28.8 km/h to 90 km/h) -> triggers speed and accel alert
    Future.delayed(const Duration(milliseconds: 600), () {
      streamController.add(LocationData(latitude: 0.001, longitude: 0.001, accuracy: 1, altitude: 0, speed: 25.0, speedAccuracy: 0, heading: 0, timestamp: t2));
    });

    // 3. Sudden brake (25 m/s to 3 m/s = 90 km/h to 10.8 km/h) -> triggers braking alert
    Future.delayed(const Duration(milliseconds: 1200), () {
      streamController.add(LocationData(latitude: 0.002, longitude: 0.002, accuracy: 1, altitude: 0, speed: 3.0, speedAccuracy: 0, heading: 0, timestamp: t3));
    });

    Future.delayed(const Duration(milliseconds: 1800), () {
      simulatedSpeedMonitor.dispose();
      streamController.close();

      // Copy simulation logs to the UI logs
      setState(() {
        _speedLogs.insertAll(0, simulatedLogs.reversed);
      });

      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Simulation trip finished! Alerts added to logs.')),
      );
    });
  }

  // Opens a bottom sheet displaying beautiful GeoJSON data
  void _showGeoJsonExporterSheet(Map<String, dynamic> geoJsonMap) {
    final prettyString = const JsonEncoder.withIndent('  ').convert(geoJsonMap);
    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      builder: (context) {
        return Container(
          height: MediaQuery.of(context).size.height * 0.7,
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  const Text('GeoJSON Export Output', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                  IconButton(
                    icon: const Icon(Icons.copy),
                    onPressed: () {
                      Clipboard.setData(ClipboardData(text: prettyString));
                      ScaffoldMessenger.of(context).showSnackBar(
                        const SnackBar(content: Text('Copied GeoJSON to clipboard!')),
                      );
                    },
                  ),
                ],
              ),
              const Divider(),
              Expanded(
                child: SingleChildScrollView(
                  child: Container(
                    padding: const EdgeInsets.all(12),
                    decoration: BoxDecoration(
                      color: Colors.grey.shade900,
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: Text(
                      prettyString,
                      style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
                    ),
                  ),
                ),
              ),
              const SizedBox(height: 12),
              FilledButton(
                onPressed: () => Navigator.pop(context),
                child: const Text('Close'),
              ),
            ],
          ),
        );
      },
    );
  }

  @override
  void dispose() {
    _streamSubscription?.cancel();
    _recordTimer?.cancel();
    _tabController.dispose();
    _speedAlertSub?.cancel();
    _gForceAlertSub?.cancel();
    _drEngine.stop();
    SmartLocation.geofence.dispose();
    SmartLocation.speed.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Smart Location Toolkit'),
        bottom: TabBar(
          controller: _tabController,
          isScrollable: true,
          tabs: const [
            Tab(icon: Icon(Icons.gps_fixed), text: 'Core GPS'),
            Tab(icon: Icon(Icons.security), text: 'Privacy'),
            Tab(icon: Icon(Icons.route), text: 'Recorder'),
            Tab(icon: Icon(Icons.adjust), text: 'Geofence'),
            Tab(icon: Icon(Icons.speed), text: 'Speed'),
            Tab(icon: Icon(Icons.science), text: 'Advanced'),
          ],
        ),
      ),
      body: _isLoading
          ? const Center(child: CircularProgressIndicator())
          : _errorMsg.isNotEmpty
              ? Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Text('Error: $_errorMsg', style: const TextStyle(color: Colors.red)),
                      const SizedBox(height: 16),
                      ElevatedButton(onPressed: _initLocation, child: const Text('Retry')),
                    ],
                  ),
                )
              : TabBarView(
                  controller: _tabController,
                  children: [
                    _buildCoreGPSView(),
                    _buildPrivacyView(),
                    _buildRouteRecorderView(),
                    _buildGeofencingView(),
                    _buildSpeedMonitorView(),
                    _buildAdvancedFeaturesView(),
                  ],
                ),
    );
  }

  Widget _buildCoreGPSView() {
    final loc = _currentLocation;
    final isStreaming = _streamSubscription != null;
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          // Mock Location Banner
          if (loc != null)
            Card(
              color: loc.isMocked ? Colors.red.shade900 : Colors.green.shade900,
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Row(
                  children: [
                    Icon(
                      loc.isMocked ? Icons.warning : Icons.verified,
                      size: 32,
                      color: Colors.white,
                    ),
                    const SizedBox(width: 16),
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            loc.isMocked ? 'MOCKED GPS DETECTED' : 'SECURE GPS SIGNAL',
                            style: const TextStyle(
                              fontSize: 18,
                              fontWeight: FontWeight.bold,
                              color: Colors.white,
                            ),
                          ),
                          Text(
                            loc.isMocked
                                ? 'This location coordinates have been spoofed.'
                                : 'Coordinates are authentic from device hardware.',
                            style: const TextStyle(color: Colors.white70),
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            ),
          const SizedBox(height: 16),
          if (loc != null) ...[
            _buildDataCard('Latitude', '${loc.latitude}°'),
            _buildDataCard('Longitude', '${loc.longitude}°'),
            _buildDataCard('Accuracy', '${loc.accuracy.toStringAsFixed(1)} meters'),
            _buildDataCard('Altitude', '${loc.altitude.toStringAsFixed(1)} meters'),
            _buildDataCard('Speed', '${(loc.speed * 3.6).toStringAsFixed(1)} km/h'),
            _buildDataCard('Heading', '${loc.heading.toStringAsFixed(0)}°'),
            _buildDataCard('Last Updated', loc.timestamp?.toIso8601String() ?? 'Unknown'),
          ] else
            const Center(child: Text('No location fetched yet.')),
          const SizedBox(height: 24),
          Row(
            children: [
              Expanded(
                child: ElevatedButton.icon(
                  onPressed: _initLocation,
                  icon: const Icon(Icons.refresh),
                  label: const Text('Fetch One-Time'),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: FilledButton.icon(
                  onPressed: () => _toggleLiveStream(!isStreaming),
                  icon: Icon(isStreaming ? Icons.gps_off : Icons.gps_fixed),
                  label: Text(isStreaming ? 'Stop Stream' : 'Live Stream'),
                  style: FilledButton.styleFrom(
                    backgroundColor: isStreaming ? Colors.orange.shade800 : null,
                  ),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }

  Widget _buildPrivacyView() {
    final loc = _currentLocation;
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const Text(
            'Location Fuzzing & Privacy Guard',
            style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          const Text(
            'Reduce precision or add random offsets to respect user privacy settings before uploading locations to your backend.',
            style: TextStyle(color: Colors.grey),
          ),
          const SizedBox(height: 16),
          if (loc != null) ...[
            _buildPrivacyCompareCard(
              title: 'Location Fuzzing',
              subtitle: 'Adds random displacement within a given radius.',
              original: '${loc.latitude.toStringAsFixed(6)}, ${loc.longitude.toStringAsFixed(6)}',
              modified: _fuzzedLocation != null
                  ? '${_fuzzedLocation!.latitude.toStringAsFixed(6)}, ${_fuzzedLocation!.longitude.toStringAsFixed(6)}'
                  : 'N/A',
              controlWidget: Slider(
                value: _fuzzRadius,
                min: 100,
                max: 2000,
                divisions: 19,
                label: '${_fuzzRadius.round()}m',
                onChanged: (val) {
                  setState(() {
                    _fuzzRadius = val;
                  });
                  _updatePrivacyStates(loc);
                },
              ),
              footerText: 'Fuzzing radius: ${_fuzzRadius.round()} meters',
            ),
            const SizedBox(height: 16),
            _buildPrivacyCompareCard(
              title: 'Coarse Grid Snapping',
              subtitle: 'Snaps coordinates to a grid resolution.',
              original: '${loc.latitude.toStringAsFixed(6)}, ${loc.longitude.toStringAsFixed(6)}',
              modified: _snappedLocation != null
                  ? '${_snappedLocation!.latitude.toStringAsFixed(6)}, ${_snappedLocation!.longitude.toStringAsFixed(6)}'
                  : 'N/A',
              controlWidget: Slider(
                value: _gridSize,
                min: 0.001,
                max: 0.1,
                divisions: 99,
                label: _gridSize.toStringAsFixed(3),
                onChanged: (val) {
                  setState(() {
                    _gridSize = val;
                  });
                  _updatePrivacyStates(loc);
                },
              ),
              footerText: 'Grid Resolution: ${_gridSize.toStringAsFixed(3)} degrees (~${(_gridSize * 111).toStringAsFixed(1)} km)',
            ),
            const SizedBox(height: 20),
            ElevatedButton.icon(
              onPressed: () => _showGeoJsonExporterSheet(GeoJsonExporter.toPointMap(loc)),
              icon: const Icon(Icons.code),
              label: const Text('Export Current Location to GeoJSON Point'),
            ),
          ] else
            const Center(child: Text('Please fetch core GPS location first.')),
        ],
      ),
    );
  }

  Widget _buildRouteRecorderView() {
    final showSimplificationStats = _recordedPath.isNotEmpty;
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const Text(
            'Route Recorder & Map Utilities',
            style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                children: [
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceAround,
                    children: [
                      _buildStatColumn('Duration', _formatDuration(_recordElapsed)),
                      _buildStatColumn('Distance', '${_recordedDistance.toStringAsFixed(1)} m'),
                      _buildStatColumn('Points', '${_isRecording ? SmartLocation.recorder.points.length : _recordedPath.length}'),
                    ],
                  ),
                  const SizedBox(height: 20),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      if (!_isRecording)
                        FilledButton.icon(
                          onPressed: _startRecording,
                          icon: const Icon(Icons.play_arrow),
                          label: const Text('Start Recording'),
                        ),
                      if (_isRecording) ...[
                        IconButton.filled(
                          onPressed: _isPaused ? _resumeRecording : _pauseRecording,
                          icon: Icon(_isPaused ? Icons.play_arrow : Icons.pause),
                          style: IconButton.styleFrom(
                            backgroundColor: _isPaused ? Colors.green : Colors.orange,
                          ),
                        ),
                        const SizedBox(width: 16),
                        IconButton.filled(
                          onPressed: _stopRecording,
                          icon: const Icon(Icons.stop),
                          style: IconButton.styleFrom(backgroundColor: Colors.red),
                        ),
                      ]
                    ],
                  )
                ],
              ),
            ),
          ),
          const SizedBox(height: 24),
          if (showSimplificationStats) ...[
            ElevatedButton.icon(
              onPressed: _simplifyRecordedPath,
              icon: const Icon(Icons.compress),
              label: const Text('Simplify Path (RDP Algorithm)'),
            ),
            const SizedBox(height: 12),
            ElevatedButton.icon(
              onPressed: () => _showGeoJsonExporterSheet(
                GeoJsonExporter.toLineStringMap(_simplifiedPath.isNotEmpty ? _simplifiedPath : _recordedPath),
              ),
              icon: const Icon(Icons.code),
              label: const Text('Export Path to GeoJSON LineString'),
              style: ElevatedButton.styleFrom(backgroundColor: Colors.blueGrey.shade800),
            ),
            const SizedBox(height: 16),
            Card(
              color: Colors.blueGrey.shade900,
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text('Compressed Path Stats:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15)),
                    const Divider(height: 16),
                    Text('Original points: ${_recordedPath.length}'),
                    Text('Simplified points: ${_simplifiedPath.isNotEmpty ? _simplifiedPath.length : 'N/A'}'),
                    if (_simplifiedPath.isNotEmpty) ...[
                      Text(
                        'Compression savings: ${((1 - (_simplifiedPath.length / _recordedPath.length)) * 100).toStringAsFixed(1)}% space saved!',
                        style: const TextStyle(color: Colors.green, fontWeight: FontWeight.bold),
                      ),
                    ],
                    const SizedBox(height: 12),
                    const Text('Computed GeoBounds:', style: TextStyle(fontWeight: FontWeight.bold)),
                    Text('SW Corners: ${_routeBounds?.southwestLatitude.toStringAsFixed(5)}, ${_routeBounds?.southwestLongitude.toStringAsFixed(5)}'),
                    Text('NE Corners: ${_routeBounds?.northeastLatitude.toStringAsFixed(5)}, ${_routeBounds?.northeastLongitude.toStringAsFixed(5)}'),
                    const SizedBox(height: 12),
                    Row(
                      children: [
                        const Text('Google Encoded Polyline:', style: TextStyle(fontWeight: FontWeight.bold)),
                        const Spacer(),
                        if (_encodedPolyline.isNotEmpty)
                          IconButton(
                            icon: const Icon(Icons.copy, size: 18),
                            constraints: const BoxConstraints(),
                            padding: EdgeInsets.zero,
                            onPressed: () {
                              Clipboard.setData(ClipboardData(text: _encodedPolyline));
                              ScaffoldMessenger.of(context).showSnackBar(
                                const SnackBar(content: Text('Polyline copied to clipboard!')),
                              );
                            },
                          ),
                      ],
                    ),
                    const SizedBox(height: 4),
                    Container(
                      width: double.infinity,
                      padding: const EdgeInsets.all(8),
                      decoration: BoxDecoration(
                        color: Colors.black26,
                        borderRadius: BorderRadius.circular(4),
                      ),
                      child: Text(
                        _encodedPolyline.isNotEmpty ? _encodedPolyline : 'Empty Path',
                        style: const TextStyle(fontFamily: 'monospace', fontSize: 11, color: Colors.yellow),
                        maxLines: 2,
                        overflow: TextOverflow.ellipsis,
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ] else
            const Card(
              child: Padding(
                padding: EdgeInsets.all(16.0),
                child: Text(
                  'Start a recording and walk around to gather coordinate points. The path will compile statistics and compression bounds.',
                  textAlign: TextAlign.center,
                ),
              ),
            )
        ],
      ),
    );
  }

  Widget _buildGeofencingView() {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const Text(
            'Interactive Geofencing Monitor',
            style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          const Text(
            'Adds a circular geofence around your initial position. Stream will trigger enter/exit/dwell events automatically.',
            style: TextStyle(color: Colors.grey),
          ),
          const SizedBox(height: 16),
          Row(
            children: [
              Expanded(
                child: Text('Radius: ${_geofenceRadius.round()} meters'),
              ),
              Expanded(
                flex: 2,
                child: Slider(
                  value: _geofenceRadius,
                  min: 10,
                  max: 300,
                  divisions: 29,
                  label: '${_geofenceRadius.round()}m',
                  onChanged: _isGeofenceMonitoring
                      ? null
                      : (val) {
                          setState(() {
                            _geofenceRadius = val;
                          });
                        },
                ),
              ),
            ],
          ),
          const SizedBox(height: 12),
          ElevatedButton.icon(
            onPressed: _isGeofenceMonitoring ? _stopGeofenceMonitoring : _setupGeofenceAndStart,
            icon: Icon(_isGeofenceMonitoring ? Icons.location_disabled : Icons.location_searching),
            label: Text(_isGeofenceMonitoring ? 'Stop Monitoring' : 'Start Geofencing'),
            style: ElevatedButton.styleFrom(
              backgroundColor: _isGeofenceMonitoring ? Colors.red.shade900 : null,
            ),
          ),
          const SizedBox(height: 20),
          const Text(
            'Transition Log History:',
            style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
          ),
          const SizedBox(height: 8),
          Expanded(
            child: _geofenceEvents.isEmpty
                ? Center(
                    child: Text(
                      _isGeofenceMonitoring
                          ? 'Waiting for movement events...'
                          : 'Press Start to begin tracking.',
                      style: const TextStyle(fontStyle: FontStyle.italic, color: Colors.grey),
                    ),
                  )
                : ListView.builder(
                    itemCount: _geofenceEvents.length,
                    itemBuilder: (context, idx) {
                      final ev = _geofenceEvents[idx];
                      IconData icon = Icons.info;
                      Color color = Colors.blue;
                      if (ev.triggerType == GeofenceTrigger.enter) {
                        icon = Icons.login;
                        color = Colors.green;
                      } else if (ev.triggerType == GeofenceTrigger.exit) {
                        icon = Icons.logout;
                        color = Colors.red;
                      } else if (ev.triggerType == GeofenceTrigger.dwell) {
                        icon = Icons.timer;
                        color = Colors.orange;
                      }
                      return Card(
                        child: ListTile(
                          leading: Icon(icon, color: color),
                          title: Text(
                            'Triggered: ${ev.triggerType.name.toUpperCase()}',
                            style: const TextStyle(fontWeight: FontWeight.bold),
                          ),
                          subtitle: Text('Geofence: ${ev.geofenceId}'),
                          trailing: Text(
                            '${ev.timestamp.hour.toString().padLeft(2, '0')}:${ev.timestamp.minute.toString().padLeft(2, '0')}:${ev.timestamp.second.toString().padLeft(2, '0')}',
                          ),
                        ),
                      );
                    },
                  ),
          ),
        ],
      ),
    );
  }

  Widget _buildSpeedMonitorView() {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const Text(
            'Speed Limit & Brake Monitor',
            style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          const Text(
            'Checks for speed limits and flags G-force violations (sudden brake or acceleration bursts).',
            style: TextStyle(color: Colors.grey),
          ),
          const SizedBox(height: 16),
          Row(
            children: [
              Expanded(
                child: Text('Limit: ${_speedLimitKmh.round()} km/h'),
              ),
              Expanded(
                flex: 2,
                child: Slider(
                  value: _speedLimitKmh,
                  min: 20,
                  max: 140,
                  divisions: 12,
                  label: '${_speedLimitKmh.round()} km/h',
                  onChanged: _isSpeedMonitoring
                      ? null
                      : (val) {
                          setState(() {
                            _speedLimitKmh = val;
                          });
                        },
                ),
              ),
            ],
          ),
          const SizedBox(height: 12),
          Row(
            children: [
              Expanded(
                child: FilledButton.icon(
                  onPressed: () => _toggleSpeedMonitoring(!_isSpeedMonitoring),
                  icon: Icon(_isSpeedMonitoring ? Icons.notifications_off : Icons.notifications_active),
                  label: Text(_isSpeedMonitoring ? 'Stop Monitor' : 'Start Monitor'),
                  style: FilledButton.styleFrom(
                    backgroundColor: _isSpeedMonitoring ? Colors.red.shade900 : null,
                  ),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: ElevatedButton.icon(
                  onPressed: _simulateTripAlerts,
                  icon: const Icon(Icons.directions_car),
                  label: const Text('Simulate Brake/Speed'),
                ),
              ),
            ],
          ),
          const SizedBox(height: 20),
          const Text(
            'Speed Event Alerts Log:',
            style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
          ),
          const SizedBox(height: 8),
          Expanded(
            child: _speedLogs.isEmpty
                ? const Center(
                    child: Text(
                      'No alerts logged. Tap "Simulate Brake/Speed" to trigger mock events.',
                      style: TextStyle(fontStyle: FontStyle.italic, color: Colors.grey),
                    ),
                  )
                : ListView.builder(
                    itemCount: _speedLogs.length,
                    itemBuilder: (context, idx) {
                      final log = _speedLogs[idx];
                      final isGForce = log.contains('BRAKE/ACCEL') || log.contains('[BRAKE/ACCEL]');
                      final isSpeed = log.contains('SPEED LIMIT') || log.contains('[SPEED WARNING]');
                      
                      Color cardColor = Colors.grey.shade900;
                      if (log.contains('braking')) {
                        cardColor = Colors.orange.shade900.withOpacity(0.4);
                      } else if (isSpeed) {
                        cardColor = Colors.red.shade900.withOpacity(0.4);
                      }

                      return Card(
                        color: cardColor,
                        child: Padding(
                          padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
                          child: Text(
                            log,
                            style: const TextStyle(fontSize: 14),
                          ),
                        ),
                      );
                    },
                  ),
          ),
        ],
      ),
    );
  }

  // Advanced Phase 2 States
  final List<String> _deadReckoningLogs = [];
  bool _isDRSimulating = false;
  final DeadReckoning _drEngine = DeadReckoning();

  double _ecoBatteryPercent = 80.0;
  bool _ecoHasNetwork = true;

  final List<String> _indoorLogs = [];
  double _rssiBeacon1 = -50.0;
  double _rssiBeacon2 = -85.0;
  double _rssiBeacon3 = -95.0;

  final List<String> _mapMatchingLogs = [];

  // Spatial Hashing states
  final SpatialHashingGrid _demoSpatialGrid = SpatialHashingGrid(gridSize: 0.01);
  final List<String> _spatialHashLogs = [];
  bool _spatialGridInitialized = false;
  double _queryLatOffset = 0.0;
  double _queryLngOffset = 0.0;

  void _runDeadReckoningSimulation() async {
    if (_isDRSimulating) return;
    setState(() {
      _isDRSimulating = true;
      _deadReckoningLogs.clear();
      _deadReckoningLogs.add('Starting Dead Reckoning Simulation...');
      _deadReckoningLogs.add('GPS stream started (1 update/sec).');
    });

    final streamController = StreamController<LocationData>();
    _drEngine.start(
      gpsStream: streamController.stream,
      onUpdate: (loc) {
        if (mounted) {
          setState(() {
            _deadReckoningLogs.insert(0, '[${loc.isMocked ? "DR EXTRAPOLATION" : "GPS RAW"}] Lat: ${loc.latitude.toStringAsFixed(6)}, Lng: ${loc.longitude.toStringAsFixed(6)}, Acc: ${loc.accuracy.toStringAsFixed(1)}m');
          });
        }
      },
      staleThreshold: const Duration(seconds: 2),
      extrapolationInterval: const Duration(seconds: 1),
    );

    double lat = 37.7749;
    double lng = -122.4194;

    for (int i = 0; i < 3; i++) {
      if (!mounted) return;
      await Future.delayed(const Duration(seconds: 1));
      lng += 0.00015;
      streamController.add(LocationData(
        latitude: lat,
        longitude: lng,
        accuracy: 8.0,
        altitude: 0.0,
        speed: 15.0, // 15 m/s (~54 km/h)
        speedAccuracy: 0.0,
        heading: 90.0, // East
        timestamp: DateTime.now(),
      ));
    }

    if (!mounted) return;
    setState(() {
      _deadReckoningLogs.insert(0, '⚠️ GPS SIGNAL LOST! Extrapolating heading/speed...');
    });

    await Future.delayed(const Duration(seconds: 4));

    if (!mounted) return;
    setState(() {
      _deadReckoningLogs.insert(0, '🔄 GPS SIGNAL REGAINED. Resuming clean updates...');
    });

    for (int i = 0; i < 2; i++) {
      if (!mounted) return;
      lat -= 0.0001;
      streamController.add(LocationData(
        latitude: lat,
        longitude: lng,
        accuracy: 9.0,
        altitude: 0.0,
        speed: 10.0,
        speedAccuracy: 0.0,
        heading: 180.0, // South
        timestamp: DateTime.now(),
      ));
      await Future.delayed(const Duration(seconds: 1));
    }

    _drEngine.stop();
    await streamController.close();
    if (mounted) {
      setState(() {
        _isDRSimulating = false;
        _deadReckoningLogs.insert(0, 'Simulation complete.');
      });
    }
  }

  void _runMapMatchingDemo() {
    setState(() {
      _mapMatchingLogs.clear();
      _mapMatchingLogs.add('Running Map Snapping & Smoothing Demo...');
    });

    final List<LocationData> rawNoisyPath = [
      LocationData(latitude: 37.7749, longitude: -122.4194, accuracy: 5.0, altitude: 0, speed: 10, speedAccuracy: 0, heading: 0, timestamp: DateTime(2026, 1, 1, 12, 0, 0)),
      LocationData(latitude: 37.7850, longitude: -122.4150, accuracy: 5.0, altitude: 0, speed: 10, speedAccuracy: 0, heading: 0, timestamp: DateTime(2026, 1, 1, 12, 0, 1)), // Jitter/Outlier (~1.2km jump in 1s)
      LocationData(latitude: 37.7752, longitude: -122.4180, accuracy: 5.0, altitude: 0, speed: 10, speedAccuracy: 0, heading: 0, timestamp: DateTime(2026, 1, 1, 12, 0, 2)),
      LocationData(latitude: 37.7755, longitude: -122.4170, accuracy: 5.0, altitude: 0, speed: 10, speedAccuracy: 0, heading: 0, timestamp: DateTime(2026, 1, 1, 12, 0, 3)),
    ];

    final List<LocationData> roadSegments = [
      LocationData(latitude: 37.7750, longitude: -122.4200, accuracy: 0, altitude: 0, speed: 0, speedAccuracy: 0, heading: 0),
      LocationData(latitude: 37.7750, longitude: -122.4150, accuracy: 0, altitude: 0, speed: 0, speedAccuracy: 0, heading: 0),
    ];

    final smoothed = SmartLocation.matching.smoothPath(rawNoisyPath);
    final snapped = SmartLocation.matching.snapToNetwork(smoothed, roadSegments);

    setState(() {
      _mapMatchingLogs.add('Original Path size: ${rawNoisyPath.length} points.');
      _mapMatchingLogs.add('Smoothed Path size: ${smoothed.length} points (filtered outlier!).');
      _mapMatchingLogs.add('Snapping to Road Segment (Lat: 37.7750)...');
      for (int i = 0; i < snapped.length; i++) {
        final orig = smoothed[i];
        final snap = snapped[i];
        _mapMatchingLogs.add('Point ${i+1}: Raw(${orig.latitude.toStringAsFixed(6)}, ${orig.longitude.toStringAsFixed(6)}) ➔ Snapped(${snap.latitude.toStringAsFixed(6)}, ${snap.longitude.toStringAsFixed(6)})');
      }
      _mapMatchingLogs.add('Demo Finished!');
    });
  }

  void _runIndoorCentroidLocate() {
    final emulator = IndoorEmulator();
    final lobby = LocationData(latitude: 37.774900, longitude: -122.419400, accuracy: 2.0, altitude: 0, speed: 0, speedAccuracy: 0, heading: 0);
    final confRoom = LocationData(latitude: 37.775200, longitude: -122.419000, accuracy: 3.0, altitude: 0, speed: 0, speedAccuracy: 0, heading: 0);
    final kitchen = LocationData(latitude: 37.774500, longitude: -122.418500, accuracy: 4.0, altitude: 0, speed: 0, speedAccuracy: 0, heading: 0);

    emulator.registerBeacons({
      'Lobby_AP': lobby,
      'ConfRoom_AP': confRoom,
      'Kitchen_AP': kitchen,
    });

    final signals = {
      'Lobby_AP': _rssiBeacon1,
      'ConfRoom_AP': _rssiBeacon2,
      'Kitchen_AP': _rssiBeacon3,
    };

    final estimated = emulator.locateBySignals(signals);

    setState(() {
      _indoorLogs.clear();
      _indoorLogs.add('Scanning WiFi Signal Strengths:');
      signals.forEach((ssid, rssi) {
        _indoorLogs.add(' • $ssid: ${rssi.round()} dBm');
      });
      _indoorLogs.add('Centroid estimation running...');
      if (estimated != null) {
        _indoorLogs.add('Estimated Indoor Coordinate:');
        _indoorLogs.add('Lat: ${estimated.latitude.toStringAsFixed(6)}');
        _indoorLogs.add('Lng: ${estimated.longitude.toStringAsFixed(6)}');
        _indoorLogs.add('Accuracy base: ${estimated.accuracy.toStringAsFixed(1)}m');
      } else {
        _indoorLogs.add('WiFi signals too weak to locate.');
      }
    });
  }

  void _initSpatialGridDemo() {
    _demoSpatialGrid.clear();
    _spatialHashLogs.clear();
    _spatialHashLogs.add('Initializing Spatial Hashing Grid...');

    final double baseLat = 37.7749;
    final double baseLng = -122.4194;

    int count = 0;
    // We add geofences at different offsets: from -0.05 to +0.05 in steps of 0.01
    // This creates an 11x11 grid of 121 geofences
    for (double dLat = -0.05; dLat <= 0.05; dLat += 0.01) {
      for (double dLng = -0.05; dLng <= 0.05; dLng += 0.01) {
        final id = 'gf_${(dLat * 100).round()}_${(dLng * 100).round()}';
        final double lat = double.parse((baseLat + dLat).toStringAsFixed(6));
        final double lng = double.parse((baseLng + dLng).toStringAsFixed(6));
        final gf = Geofence(
          id: id,
          latitude: lat,
          longitude: lng,
          radiusInMeters: 50.0,
        );
        _demoSpatialGrid.addGeofence(gf);
        count++;
      }
    }

    setState(() {
      _spatialGridInitialized = true;
      _spatialHashLogs.add('Added $count geofences to the Spatial Hash Grid.');
      _spatialHashLogs.add('Grid size resolution: 0.01 degrees (~1.1 km).');
      _spatialHashLogs.add('Press "Query Nearby" to test lookup optimization.');
    });
  }

  void _runSpatialGridQuery() {
    if (!_spatialGridInitialized) {
      _initSpatialGridDemo();
    }

    final double baseLat = 37.7749;
    final double baseLng = -122.4194;

    final double queryLat = double.parse((baseLat + _queryLatOffset).toStringAsFixed(6));
    final double queryLng = double.parse((baseLng + _queryLngOffset).toStringAsFixed(6));

    final queryKey = _demoSpatialGrid.getGridKey(queryLat, queryLng);

    // Perform spatial query
    final nearby = _demoSpatialGrid.getNearbyGeofences(queryLat, queryLng);

    setState(() {
      _spatialHashLogs.insert(0, '----------------------------------------');
      _spatialHashLogs.insert(0, '🚀 Scan performance: Checked 9 grid cells.');
      _spatialHashLogs.insert(0, '💡 Optimization: Avoided checking all other cells!');
      _spatialHashLogs.insert(0, '📍 Found ${nearby.length} geofence(s) in local vicinity.');
      if (nearby.isNotEmpty) {
        for (final gf in nearby.take(5)) {
          final dist = SmartLocation.distanceBetween(queryLat, queryLng, gf.latitude, gf.longitude);
          _spatialHashLogs.insert(0, ' • ${gf.id} at (${gf.latitude}, ${gf.longitude}) ~${dist.round()}m away');
        }
        if (nearby.length > 5) {
          _spatialHashLogs.insert(0, ' • ... and ${nearby.length - 5} more.');
        }
      }
      _spatialHashLogs.insert(0, '🌐 Query Cell Key: "$queryKey"');
      _spatialHashLogs.insert(0, '🔍 Querying coordinates: ($queryLat, $queryLng)');
    });
  }

  Widget _buildAdvancedFeaturesView() {
    final ecoAccuracy = SmartLocation.eco.getOptimizedAccuracy(
      batteryLevelPercent: _ecoBatteryPercent,
      hasNetwork: _ecoHasNetwork,
    );
    final ecoDistance = SmartLocation.eco.getOptimizedDistanceFilter(
      batteryLevelPercent: _ecoBatteryPercent,
      hasNetwork: _ecoHasNetwork,
    );

    return SingleChildScrollView(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const Text(
            'Phase 2 Advanced Location Features',
            style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.deepPurpleAccent),
          ),
          const SizedBox(height: 8),
          const Text(
            'Demo panels for cutting edge algorithms including Dead Reckoning, Map Snapping, Hashing, Eco Optimization, and WiFi centroid localization.',
            style: TextStyle(color: Colors.grey, fontSize: 13),
          ),
          const SizedBox(height: 16),

          // 1. Dead Reckoning Card
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  const Text('1. Dead Reckoning kinematic engine', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
                  const SizedBox(height: 4),
                  const Text('Simulates 3 seconds of GPS updates, then introduces 4 seconds of GPS loss to trigger automated kinematic vector extrapolation (s = v * t).', style: TextStyle(color: Colors.grey, fontSize: 12)),
                  const SizedBox(height: 12),
                  ElevatedButton.icon(
                    onPressed: _isDRSimulating ? null : _runDeadReckoningSimulation,
                    icon: const Icon(Icons.rocket_launch),
                    label: Text(_isDRSimulating ? 'Simulating Extrapolation...' : 'Simulate GPS Signal Loss'),
                  ),
                  const SizedBox(height: 12),
                  Container(
                    height: 120,
                    padding: const EdgeInsets.all(8),
                    decoration: BoxDecoration(
                      color: Colors.black38,
                      borderRadius: BorderRadius.circular(6),
                    ),
                    child: _deadReckoningLogs.isEmpty
                        ? const Center(child: Text('Press button to start simulation log', style: TextStyle(color: Colors.grey, fontStyle: FontStyle.italic)))
                        : ListView.builder(
                            itemCount: _deadReckoningLogs.length,
                            itemBuilder: (ctx, i) => Text(_deadReckoningLogs[i], style: const TextStyle(fontFamily: 'monospace', fontSize: 11)),
                          ),
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),

          // 2. Eco Optimizer Card
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  const Text('2. Battery & Network Eco-Optimizer', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
                  const SizedBox(height: 8),
                  Row(
                    children: [
                      Expanded(
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text('Battery: ${_ecoBatteryPercent.round()}%'),
                            Slider(
                              value: _ecoBatteryPercent,
                              min: 1,
                              max: 100,
                              onChanged: (v) => setState(() => _ecoBatteryPercent = v),
                            ),
                          ],
                        ),
                      ),
                      const SizedBox(width: 16),
                      Column(
                        children: [
                          const Text('Network AP', style: TextStyle(fontSize: 12, color: Colors.grey)),
                          Switch(
                            value: _ecoHasNetwork,
                            onChanged: (v) => setState(() => _ecoHasNetwork = v),
                          ),
                        ],
                      ),
                    ],
                  ),
                  const Divider(),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceAround,
                    children: [
                      _buildStatColumn('Accuracy Setting', ecoAccuracy.name.toUpperCase()),
                      _buildStatColumn('Distance Filter', '$ecoDistance meters'),
                    ],
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),

          // 3. Indoor Wifi AP Centroid Card
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  const Text('3. Indoor WiFi Fingerprint Locator', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
                  const SizedBox(height: 4),
                  const Text('Simulates scanning Wi-Fi signal strengths to determine a weighted centroid of access points.', style: TextStyle(color: Colors.grey, fontSize: 12)),
                  const SizedBox(height: 12),
                  Text('Lobby AP (37.7749, -122.4194) Signal: ${_rssiBeacon1.round()} dBm'),
                  Slider(
                    value: _rssiBeacon1,
                    min: -100,
                    max: -30,
                    onChanged: (v) => setState(() => _rssiBeacon1 = v),
                  ),
                  Text('ConfRoom AP (37.7752, -122.4190) Signal: ${_rssiBeacon2.round()} dBm'),
                  Slider(
                    value: _rssiBeacon2,
                    min: -100,
                    max: -30,
                    onChanged: (v) => setState(() => _rssiBeacon2 = v),
                  ),
                  Text('Kitchen AP (37.7745, -122.4185) Signal: ${_rssiBeacon3.round()} dBm'),
                  Slider(
                    value: _rssiBeacon3,
                    min: -100,
                    max: -30,
                    onChanged: (v) => setState(() => _rssiBeacon3 = v),
                  ),
                  ElevatedButton.icon(
                    onPressed: _runIndoorCentroidLocate,
                    icon: const Icon(Icons.wifi_find),
                    label: const Text('Calculate Indoor Position'),
                  ),
                  const SizedBox(height: 12),
                  Container(
                    height: 120,
                    padding: const EdgeInsets.all(8),
                    decoration: BoxDecoration(
                      color: Colors.black38,
                      borderRadius: BorderRadius.circular(6),
                    ),
                    child: _indoorLogs.isEmpty
                        ? const Center(child: Text('Press button to calculate indoor position', style: TextStyle(color: Colors.grey, fontStyle: FontStyle.italic)))
                        : ListView.builder(
                            itemCount: _indoorLogs.length,
                            itemBuilder: (ctx, i) => Text(_indoorLogs[i], style: const TextStyle(fontFamily: 'monospace', fontSize: 11)),
                          ),
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),

          // 4. Map Snapping and Smoothing Card
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  const Text('4. Road Snapping & Path Smoothing', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
                  const SizedBox(height: 4),
                  const Text('Filters a noisy coordinates path containing a GPS jump anomaly (15 km/h limit breached) and snaps the smoothed path onto a straight line segment.', style: TextStyle(color: Colors.grey, fontSize: 12)),
                  const SizedBox(height: 12),
                  ElevatedButton.icon(
                    onPressed: _runMapMatchingDemo,
                    icon: const Icon(Icons.map),
                    label: const Text('Run Snapping & Smoothing Demo'),
                  ),
                  const SizedBox(height: 12),
                  Container(
                    height: 120,
                    padding: const EdgeInsets.all(8),
                    decoration: BoxDecoration(
                      color: Colors.black38,
                      borderRadius: BorderRadius.circular(6),
                    ),
                    child: _mapMatchingLogs.isEmpty
                        ? const Center(child: Text('Press button to start map matching logs', style: TextStyle(color: Colors.grey, fontStyle: FontStyle.italic)))
                        : ListView.builder(
                            itemCount: _mapMatchingLogs.length,
                            itemBuilder: (ctx, i) => Text(_mapMatchingLogs[i], style: const TextStyle(fontFamily: 'monospace', fontSize: 11)),
                          ),
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),

          // 5. Spatial Hashing Card
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  const Text('5. Spatial Hashing Geofence Optimizer', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
                  const SizedBox(height: 4),
                  const Text(
                    'Buckets geofences into a spatial index to avoid O(N) linear comparisons. Only queries adjacent grid cells, reducing query complexity to O(1).',
                    style: TextStyle(color: Colors.grey, fontSize: 12),
                  ),
                  const SizedBox(height: 12),
                  Row(
                    children: [
                      Expanded(
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text('Latitude Offset: ${_queryLatOffset.toStringAsFixed(3)}°'),
                            Slider(
                              value: _queryLatOffset,
                              min: -0.06,
                              max: 0.06,
                              divisions: 12,
                              onChanged: (v) {
                                setState(() {
                                  _queryLatOffset = v;
                                });
                              },
                            ),
                          ],
                        ),
                      ),
                      const SizedBox(width: 16),
                      Expanded(
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text('Longitude Offset: ${_queryLngOffset.toStringAsFixed(3)}°'),
                            Slider(
                              value: _queryLngOffset,
                              min: -0.06,
                              max: 0.06,
                              divisions: 12,
                              onChanged: (v) {
                                setState(() {
                                  _queryLngOffset = v;
                                });
                              },
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                  Row(
                    children: [
                      Expanded(
                        child: ElevatedButton.icon(
                          onPressed: _initSpatialGridDemo,
                          icon: const Icon(Icons.grid_view),
                          label: const Text('Reset/Init Grid (121 Geofences)'),
                        ),
                      ),
                      const SizedBox(width: 12),
                      Expanded(
                        child: FilledButton.icon(
                          onPressed: _runSpatialGridQuery,
                          icon: const Icon(Icons.search),
                          label: const Text('Query Nearby'),
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 12),
                  Container(
                    height: 120,
                    padding: const EdgeInsets.all(8),
                    decoration: BoxDecoration(
                      color: Colors.black38,
                      borderRadius: BorderRadius.circular(6),
                    ),
                    child: _spatialHashLogs.isEmpty
                        ? const Center(
                            child: Text(
                              'Press "Query Nearby" or "Reset/Init" to populate log',
                              style: TextStyle(color: Colors.grey, fontStyle: FontStyle.italic),
                            ),
                          )
                        : ListView.builder(
                            itemCount: _spatialHashLogs.length,
                            itemBuilder: (ctx, i) => Text(
                              _spatialHashLogs[i],
                              style: const TextStyle(fontFamily: 'monospace', fontSize: 11),
                            ),
                          ),
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 24),
        ],
      ),
    );
  }

  Widget _buildDataCard(String title, String val) {
    return Card(
      margin: const EdgeInsets.symmetric(vertical: 4),
      child: ListTile(
        title: Text(title, style: const TextStyle(fontSize: 14, color: Colors.grey)),
        trailing: Text(val, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
      ),
    );
  }

  Widget _buildPrivacyCompareCard({
    required String title,
    required String subtitle,
    required String original,
    required String modified,
    required Widget controlWidget,
    required String footerText,
  }) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            Text(subtitle, style: const TextStyle(fontSize: 12, color: Colors.grey)),
            const Divider(height: 20),
            Row(
              children: [
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      const Text('Original Coordinates', style: TextStyle(fontSize: 11, color: Colors.grey)),
                      Text(original, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold)),
                    ],
                  ),
                ),
                const Icon(Icons.arrow_forward),
                const SizedBox(width: 8),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      const Text('Obfuscated Coordinates', style: TextStyle(fontSize: 11, color: Colors.grey)),
                      Text(modified, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: Colors.blue)),
                    ],
                  ),
                ),
              ],
            ),
            const SizedBox(height: 12),
            controlWidget,
            Align(
              alignment: Alignment.centerRight,
              child: Text(footerText, style: const TextStyle(fontSize: 11, color: Colors.grey)),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildStatColumn(String title, String val) {
    return Column(
      children: [
        Text(title, style: const TextStyle(color: Colors.grey, fontSize: 12)),
        const SizedBox(height: 4),
        Text(val, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
      ],
    );
  }

  String _formatDuration(Duration d) {
    String twoDigits(int n) => n.toString().padLeft(2, '0');
    final minutes = twoDigits(d.inMinutes.remainder(60));
    final seconds = twoDigits(d.inSeconds.remainder(60));
    return '$minutes:$seconds';
  }
}
5
likes
160
points
549
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Smart Location is a Flutter location plugin for GPS tracking, real-time location updates, geofencing, distance calculation.

Homepage

Topics

#location #gps #geolocation #tracking #flutter-location

License

MIT (license)

Dependencies

flutter, geolocator

More

Packages that depend on smart_location