feat(wip): implement trip persistence through a local repository. Include loaded trips in the start page UI
This commit is contained in:
221
frontend/lib/presentation/widgets/trip_map.dart
Normal file
221
frontend/lib/presentation/widgets/trip_map.dart
Normal file
@@ -0,0 +1,221 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user