Files
anyway/frontend/lib/presentation/widgets/trip_map.dart

222 lines
6.2 KiB
Dart

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<TripMap> createState() => _TripMapState();
}
class _TripMapState extends State<TripMap> {
GoogleMapController? _controller;
LatLng? _startLatLng;
Set<Marker> _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<void> _refreshMarkers() async {
if (_startLatLng == null) {
setState(() => _markers = const {});
return;
}
final targets = widget.showRoute
? widget.trip.landmarks
: widget.trip.landmarks.isEmpty
? const <Landmark>[]
: <Landmark>[widget.trip.landmarks.first];
if (targets.isEmpty) {
setState(() => _markers = const {});
return;
}
final markerSet = <Marker>{};
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<Polyline> _buildPolylines() {
if (!widget.showRoute) {
return const <Polyline>{};
}
final points = widget.trip.landmarks;
if (points.length < 2) {
return const <Polyline>{};
}
final polylines = <Polyline>{};
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;
}
}