222 lines
6.2 KiB
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;
|
|
}
|
|
}
|
|
|