import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:widget_to_marker/widget_to_marker.dart'; import 'package:anyway/core/constants.dart'; import 'package:anyway/domain/entities/landmark.dart'; import 'package:anyway/domain/entities/trip.dart'; import 'package:anyway/presentation/utils/trip_location_utils.dart'; import 'package:anyway/presentation/widgets/trip_marker_graphic.dart'; class TripMap extends StatefulWidget { const TripMap({ super.key, required this.trip, this.showRoute = false, this.interactive = false, this.height, this.borderRadius = 12, this.enableMyLocation = false, }); final Trip trip; final bool showRoute; final bool interactive; final double? height; final double borderRadius; final bool enableMyLocation; @override State createState() => _TripMapState(); } class _TripMapState extends State { GoogleMapController? _controller; LatLng? _startLatLng; Set _markers = const {}; @override void initState() { super.initState(); _startLatLng = _extractStartLatLng(); _refreshMarkers(); } @override void didUpdateWidget(covariant TripMap oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.trip.uuid != widget.trip.uuid) { _startLatLng = _extractStartLatLng(); _animateToStart(); } if (oldWidget.trip.landmarks != widget.trip.landmarks || oldWidget.showRoute != widget.showRoute || oldWidget.interactive != widget.interactive) { _startLatLng = _extractStartLatLng(); } _refreshMarkers(); } LatLng? _extractStartLatLng() { final coords = TripLocationUtils.startCoordinates(widget.trip); if (coords == null) { return null; } return LatLng(coords[0], coords[1]); } void _animateToStart() { // TODO - required? if (_controller == null || _startLatLng == null) { return; } _controller!.animateCamera(CameraUpdate.newLatLng(_startLatLng!)); } Future _refreshMarkers() async { if (_startLatLng == null) { setState(() => _markers = const {}); return; } final targets = widget.showRoute ? widget.trip.landmarks : widget.trip.landmarks.isEmpty ? const [] : [widget.trip.landmarks.first]; if (targets.isEmpty) { setState(() => _markers = const {}); return; } final markerSet = {}; for (var i = 0; i < targets.length; i++) { final landmark = targets[i]; final latLng = _latLngFromLandmark(landmark); if (latLng == null) continue; final descriptor = await TripMarkerGraphic( landmark: landmark, position: i + 1, compact: !widget.showRoute, ).toBitmapDescriptor( // use sizes based on font size to keep markers consistent: imageSize: Size(500, 500), ); markerSet.add( Marker( markerId: MarkerId('landmark-${landmark.uuid}'), // since the marker is a cirlce (not a pin), we center it anchor: const Offset(0, 0), position: latLng, icon: descriptor, // TODO - don't use info window but bind taps to the bottom panel // infoWindow: InfoWindow(title: landmark.name), ), ); } if (!mounted) return; setState(() => _markers = markerSet); } LatLng? _latLngFromLandmark(Landmark landmark) { if (landmark.location.length < 2) { return null; } return LatLng(landmark.location[0], landmark.location[1]); } Set _buildPolylines() { if (!widget.showRoute) { return const {}; } final points = widget.trip.landmarks; if (points.length < 2) { return const {}; } final polylines = {}; for (var i = 0; i < points.length - 1; i++) { final current = points[i]; final next = points[i + 1]; if (current.location.length < 2 || next.location.length < 2) { continue; } polylines.add( Polyline( polylineId: PolylineId('segment-${current.uuid}-${next.uuid}'), points: [ LatLng(current.location[0], current.location[1]), LatLng(next.location[0], next.location[1]), ], width: 4, color: points[i].isVisited && next.isVisited ? Colors.grey : PRIMARY_COLOR, ), ); } return polylines; } @override Widget build(BuildContext context) { if (_startLatLng == null) { return Container( height: widget.height, decoration: BoxDecoration( color: Colors.grey.shade200, borderRadius: BorderRadius.circular(widget.borderRadius), ), alignment: Alignment.center, child: const Text('No landmarks available yet'), ); } final map = GoogleMap( key: ValueKey( 'trip-map-${widget.trip.uuid}-${widget.showRoute}-${widget.interactive}', ), onMapCreated: (controller) { _controller = controller; _animateToStart(); }, initialCameraPosition: CameraPosition( target: _startLatLng!, zoom: widget.interactive ? 12.5 : 13.5, ), markers: _markers, polylines: _buildPolylines(), mapToolbarEnabled: widget.interactive, zoomControlsEnabled: widget.interactive, zoomGesturesEnabled: widget.interactive, scrollGesturesEnabled: widget.interactive, rotateGesturesEnabled: widget.interactive, tiltGesturesEnabled: widget.interactive, liteModeEnabled: !widget.interactive, myLocationEnabled: widget.enableMyLocation && widget.interactive, myLocationButtonEnabled: widget.enableMyLocation && widget.interactive, compassEnabled: widget.interactive, cloudMapId: MAP_ID, ); Widget decoratedMap = map; if (widget.height != null) { decoratedMap = SizedBox(height: widget.height, child: decoratedMap); } if (widget.borderRadius > 0) { decoratedMap = ClipRRect( borderRadius: BorderRadius.circular(widget.borderRadius), child: decoratedMap, ); } return decoratedMap; } }