import 'package:bloc/bloc.dart'; import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_cache/flutter_map_cache.dart'; import 'package:flutter_map_compass/flutter_map_compass.dart'; import 'package:intl/intl.dart'; import 'package:latlong2/latlong.dart'; import 'package:ot_viewer_app/settings_page.dart'; import 'package:ot_viewer_app/user_path_bloc.dart'; import 'package:ot_viewer_app/util.dart'; import 'package:ot_viewer_app/web_socket_cubit.dart'; import 'package:path_provider/path_provider.dart'; import 'owntracks_api.dart'; import 'dart:math' as math; Future getPath() async { final cacheDirectory = await getTemporaryDirectory(); return cacheDirectory.path; } class MapPage extends StatefulWidget { const MapPage({super.key}); @override createState() => _MapPageState(); } class _MapPageState extends State { @override void initState() { super.initState(); // _fetchPoints(); } /* Future _fetchPoints() async { final api = OwntracksApi(baseUrl: '', username: '', pass: ''); print(await api.getDevices()); final points = await api.fetchPointsForDevice( user: 'yxk', device: 'meow', from: DateTime.now().subtract(const Duration(days: 1))); setState(() { _points = points.unwrapOr([]) .map((point) => LatLng(point.lat, point.lon)) .toList(); }); } */ @override Widget build(BuildContext context) { return BlocProvider( create: (context) { final cubit = LocationSubscribeCubit(); final settings_cubit = context.read(); cubit.subscribe(settings_cubit.state); settings_cubit.stream.listen((settings) => cubit.subscribe(settings)); return cubit; }, child: FutureBuilder( future: getPath(), builder: (something, temp_path) => BlocBuilder( builder: (context, state) { return FlutterMap( options: const MapOptions( initialCenter: LatLng(48.3285, 9.8942), initialZoom: 13.0, ), children: [ TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', tileProvider: CachedTileProvider( maxStale: const Duration(days: 30), store: HiveCacheStore(temp_path.data, hiveBoxName: 'HiveCacheStore')), ), ...state.activeDevices .map((id) => UserPath(device: id, settings: state)), const MapCompass.cupertino( rotationDuration: Duration(milliseconds: 600)), // CurrentLocationLayer(), TODO: add permission RichAttributionWidget( attributions: [ TextSourceAttribution( 'OpenStreetMap contributors', onTap: () => (Uri.parse('https://openstreetmap.org/copyright')), ), ], ), ], ); }, ), ), ); } } class UserPath extends StatefulWidget { UserPath({required this.device, required this.settings}); (String, String) device; SettingsState settings; @override createState() => _UserPathState(); } class _UserPathState extends State { @override Widget build(BuildContext ctx) { return BlocProvider( create: (context) { final bloc = UserPathBloc(widget.device, widget.settings); bloc.add(UserPathFullUpdate()); return bloc; }, child: BlocListener( listener: (context, state) { UserPathBloc userPathBloc = context.read(); if (state case LocationUpdateReceived(:final position, :final deviceId)) { if (userPathBloc.deviceId == state.deviceId) { context .read() .add(UserPathLiveSubscriptionUpdate(position)); } } }, child: BlocBuilder( builder: (context, state) { print("rebuild"); final _istate = state as MainUserPathState; // make markers final List _markers = []; if (state.livePoints.isNotEmpty) { _markers.add(Marker( width: 500, height: 100, point: state.livePoints.last.asLatLng, child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ GestureDetector( onTap: () => showUserLocationModalBottomSheet( context, widget.device ), child: Container( decoration: BoxDecoration( color: Colors.black.withOpacity(0.85), // border: Border.all(color: Colors.lightBlue, width: 2), TODO: add border borderRadius: BorderRadius.circular(10), boxShadow: [ // BoxShadow(color: Colors.black, blurRadius: 4) ]), padding: EdgeInsets.all(8), child: Text( "${widget.device.$1}:${widget.device.$2}", softWrap: false, ), ), ), Icon( Icons.location_history, size: 32, ) ], ), alignment: Alignment.topCenter, rotate: true, )); } // Create fancy fade-out and blended line (TODO: make distance based. use flutter_map_math) List> segments = []; List colors = []; if (state.initialPoints.isNotEmpty) { final allPoints = state.initialPoints.map((e) => LatLng(e.lat, e.lon)).toList(); final segmentCount = math.min(100, allPoints.length); final pointsPerSegment = (allPoints.length / segmentCount).ceil(); // Split the points into segments and generate colors for (int i = 0; i < allPoints.length; i += pointsPerSegment) { int end = math.min(i + pointsPerSegment + 1, allPoints.length); segments.add(allPoints.sublist(i, end)); // Calculate the color for the segment double ratio = i / allPoints.length; Color color = Color.lerp(Colors.purple, Colors.blue, ratio)!; colors.add(color); } } // Create polylines for each segment with the corresponding color List polylines = []; for (int i = 0; i < segments.length; i++) { polylines.add( Polyline( points: segments[i], strokeWidth: 4.0, color: colors[i].withOpacity( (math.pow(i, 2) / math.pow(segments.length, 2)) * 0.7 + 0.3), // Fading effect ), ); } return Stack( children: [ PolylineLayer( polylines: [ /* Polyline( points: state.initialPoints .map((e) => LatLng(e.lat, e.lon)) .toList(), strokeWidth: 4.0, color: Colors.blue, ), */ ...polylines ], ), PolylineLayer(polylines: [ Polyline( points: state.livePoints .map((e) => LatLng(e.lat, e.lon)) .toList(), strokeWidth: 4.0, color: Colors.blue.shade200, ), ]), MarkerLayer(markers: _markers) ], ); }, ), ), ); } } showUserLocationModalBottomSheet(BuildContext context, (String, String) user) { showModalBottomSheet( context: context, builder: (bsContext) { return Container( height: MediaQuery.of(bsContext).size.height * 0.26, width: MediaQuery.of(bsContext).size.width, decoration: BoxDecoration( color: Colors.black, // border: Border.all(color: Colors.blueAccent, width: 2), borderRadius: BorderRadius.only(topLeft: Radius.circular(30), topRight: Radius.circular(30)), ), padding: EdgeInsets.all(32), child: Column( children: [ Text( '${user.$1}:${user.$2}', style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), SizedBox(height: 16,), Builder( builder: (sheetContext) { final state = context.watch().state as MainUserPathState; // get user's current location // final _istate = state as MainUserPathState; if (state.livePoints.isEmpty) { return Text( "Couldn't find ${user.$1}:${user.$2}'s Location"); } final curLocation = state.livePoints.last; // MapController.of(sheetContext).camera.pointToLatLng( // math.Point(curLocation.lat, curLocation.lon)); return Column( children: [ Container( decoration: BoxDecoration( color: Colors.black, border: Border.all(color: Colors.orange, width: 2), borderRadius: BorderRadius.circular(10), ), padding: EdgeInsets.all(16), width: double.infinity, child: Column( children: [ Text( "(${curLocation.lat.toStringAsFixed(4)}, ${curLocation.lon.toStringAsFixed(4)})"), Text(DateFormat('dd.MM.yyyy - kk:mm:ss') .format(curLocation.timestamp)), StreamBuilder( // rebuild every second for that ticking effect // not hyper efficient, but it's only a text stream: Stream.periodic( const Duration(seconds: 1)), builder: (context, _) { return Text( "${formatDuration(DateTime.now().difference(curLocation.timestamp))} ago"); }), ], ), ) ], ); }, ), ], )); // title with icon, user name and device id // location as a lat, long (truncated to 4 comma spaces) // time since last location update // location to other users in a grid (2 rows) }); }