@ -11,41 +11,55 @@ import 'place.dart';
import ' place_details.dart ' ;
import ' place_details.dart ' ;
import ' place_tracker_app.dart ' ;
import ' place_tracker_app.dart ' ;
class MapConfiguration {
final List < Place > places ;
final PlaceCategory selectedCategory ;
const MapConfiguration ( {
@ required this . places ,
@ required this . selectedCategory ,
} ) : assert ( places ! = null ) ,
assert ( selectedCategory ! = null ) ;
@ override
int get hashCode = > places . hashCode ^ selectedCategory . hashCode ;
@ override
bool operator = = ( Object other ) {
if ( identical ( this , other ) ) {
return true ;
}
if ( other . runtimeType ! = runtimeType ) {
return false ;
}
return other is MapConfiguration & &
other . places = = places & &
other . selectedCategory = = selectedCategory ;
}
static MapConfiguration of ( AppState appState ) {
return MapConfiguration (
places: appState . places ,
selectedCategory: appState . selectedCategory ,
) ;
}
}
class PlaceMap extends StatefulWidget {
class PlaceMap extends StatefulWidget {
final LatLng center ;
const PlaceMap ( {
const PlaceMap ( {
Key key ,
Key key ,
this . center ,
this . center ,
} ) : super ( key: key ) ;
} ) : super ( key: key ) ;
final LatLng center ;
@ override
@ override
PlaceMapState createState ( ) = > PlaceMapState ( ) ;
PlaceMapState createState ( ) = > PlaceMapState ( ) ;
}
}
class PlaceMapState extends State < PlaceMap > {
class PlaceMapState extends State < PlaceMap > {
static Future < BitmapDescriptor > _getPlaceMarkerIcon (
BuildContext context , PlaceCategory category ) async {
switch ( category ) {
case PlaceCategory . favorite:
return BitmapDescriptor . fromAssetImage (
createLocalImageConfiguration ( context ) , ' assets/heart.png ' ) ;
break ;
case PlaceCategory . visited:
return BitmapDescriptor . fromAssetImage (
createLocalImageConfiguration ( context ) , ' assets/visited.png ' ) ;
break ;
case PlaceCategory . wantToGo:
default :
return BitmapDescriptor . defaultMarker ;
}
}
static List < Place > _getPlacesForCategory (
PlaceCategory category , List < Place > places ) {
return places . where ( ( place ) = > place . category = = category ) . toList ( ) ;
}
Completer < GoogleMapController > mapController = Completer ( ) ;
Completer < GoogleMapController > mapController = Completer ( ) ;
MapType _currentMapType = MapType . normal ;
MapType _currentMapType = MapType . normal ;
@ -60,6 +74,50 @@ class PlaceMapState extends State<PlaceMap> {
MapConfiguration _configuration ;
MapConfiguration _configuration ;
@ override
Widget build ( BuildContext context ) {
_maybeUpdateMapConfiguration ( ) ;
var state = Provider . of < AppState > ( context ) ;
return Builder ( builder: ( context ) {
/ / We need this additional builder here so that we can pass its context to
/ / _AddPlaceButtonBar ' s onSavePressed callback. This callback shows a
/ / SnackBar and to do this , we need a build context that has Scaffold as
/ / an ancestor .
return Center (
child: Stack (
children: [
GoogleMap (
onMapCreated: onMapCreated ,
initialCameraPosition: CameraPosition (
target: widget . center ,
zoom: 11.0 ,
) ,
mapType: _currentMapType ,
markers: _markers ,
onCameraMove: ( position ) = > _lastMapPosition = position . target ,
) ,
_CategoryButtonBar (
selectedPlaceCategory: state . selectedCategory ,
visible: _pendingMarker = = null ,
onChanged: _switchSelectedCategory ,
) ,
_AddPlaceButtonBar (
visible: _pendingMarker ! = null ,
onSavePressed: ( ) = > _confirmAddPlace ( context ) ,
onCancelPressed: _cancelAddPlace ,
) ,
_MapFabs (
visible: _pendingMarker = = null ,
onAddPlacePressed: _onAddPlacePressed ,
onToggleMapTypePressed: _onToggleMapTypePressed ,
) ,
] ,
) ,
) ;
} ) ;
}
Future < void > onMapCreated ( GoogleMapController controller ) async {
Future < void > onMapCreated ( GoogleMapController controller ) async {
mapController . complete ( controller ) ;
mapController . complete ( controller ) ;
_lastMapPosition = widget . center ;
_lastMapPosition = widget . center ;
@ -83,152 +141,13 @@ class PlaceMapState extends State<PlaceMap> {
) ;
) ;
}
}
Future < Marker > _createPlaceMarker ( BuildContext context , Place place ) async {
void _cancelAddPlace ( ) {
final marker = Marker (
if ( _pendingMarker ! = null ) {
markerId: MarkerId ( place . latLng . toString ( ) ) ,
setState ( ( ) {
position: place . latLng ,
_markers . remove ( _pendingMarker ) ;
infoWindow: InfoWindow (
_pendingMarker = null ;
title: place . name ,
} ) ;
snippet: ' ${ place . starRating } Star Rating ' ,
onTap: ( ) = > _pushPlaceDetailsScreen ( place ) ,
) ,
icon: await _getPlaceMarkerIcon ( context , place . category ) ,
visible: place . category = =
Provider . of < AppState > ( context , listen: false ) . selectedCategory ,
) ;
_markedPlaces [ marker ] = place ;
return marker ;
}
void _pushPlaceDetailsScreen ( Place place ) {
assert ( place ! = null ) ;
Navigator . push < void > (
context ,
MaterialPageRoute ( builder: ( context ) {
return PlaceDetails (
place: place ,
onChanged: ( value ) = > _onPlaceChanged ( value ) ,
) ;
} ) ,
) ;
}
void _onPlaceChanged ( Place value ) {
/ / Replace the place with the modified version .
final newPlaces =
List < Place > . from ( Provider . of < AppState > ( context , listen: false ) . places ) ;
final index = newPlaces . indexWhere ( ( place ) = > place . id = = value . id ) ;
newPlaces [ index ] = value ;
_updateExistingPlaceMarker ( place: value ) ;
/ / Manually update our map configuration here since our map is already
/ / updated with the new marker . Otherwise , the map would be reconfigured
/ / in the main build method due to a modified AppState .
_configuration = MapConfiguration (
places: newPlaces ,
selectedCategory:
Provider . of < AppState > ( context , listen: false ) . selectedCategory ,
) ;
Provider . of < AppState > ( context , listen: false ) . setPlaces ( newPlaces ) ;
}
void _updateExistingPlaceMarker ( { @ required Place place } ) {
var marker = _markedPlaces . keys
. singleWhere ( ( value ) = > _markedPlaces [ value ] . id = = place . id ) ;
setState ( ( ) {
final updatedMarker = marker . copyWith (
infoWindowParam: InfoWindow (
title: place . name ,
snippet:
place . starRating ! = 0 ? ' ${ place . starRating } Star Rating ' : null ,
) ,
) ;
_updateMarker ( marker: marker , updatedMarker: updatedMarker , place: place ) ;
} ) ;
}
void _updateMarker ( {
@ required Marker marker ,
@ required Marker updatedMarker ,
@ required Place place ,
} ) {
_markers . remove ( marker ) ;
_markedPlaces . remove ( marker ) ;
_markers . add ( updatedMarker ) ;
_markedPlaces [ updatedMarker ] = place ;
}
Future < void > _switchSelectedCategory ( PlaceCategory category ) async {
Provider . of < AppState > ( context , listen: false ) . setSelectedCategory ( category ) ;
await _showPlacesForSelectedCategory ( category ) ;
}
Future < void > _showPlacesForSelectedCategory ( PlaceCategory category ) async {
setState ( ( ) {
for ( var marker in List . of ( _markedPlaces . keys ) ) {
final place = _markedPlaces [ marker ] ;
final updatedMarker = marker . copyWith (
visibleParam: place . category = = category ,
) ;
_updateMarker (
marker: marker ,
updatedMarker: updatedMarker ,
place: place ,
) ;
}
} ) ;
await _zoomToFitPlaces ( _getPlacesForCategory (
category ,
_markedPlaces . values . toList ( ) ,
) ) ;
}
Future < void > _zoomToFitPlaces ( List < Place > places ) async {
var controller = await mapController . future ;
/ / Default min / max values to latitude and longitude of center .
var minLat = widget . center . latitude ;
var maxLat = widget . center . latitude ;
var minLong = widget . center . longitude ;
var maxLong = widget . center . longitude ;
for ( var place in places ) {
minLat = min ( minLat , place . latitude ) ;
maxLat = max ( maxLat , place . latitude ) ;
minLong = min ( minLong , place . longitude ) ;
maxLong = max ( maxLong , place . longitude ) ;
}
}
await controller . animateCamera (
CameraUpdate . newLatLngBounds (
LatLngBounds (
southwest: LatLng ( minLat , minLong ) ,
northeast: LatLng ( maxLat , maxLong ) ,
) ,
48.0 ,
) ,
) ;
}
Future < void > _onAddPlacePressed ( ) async {
setState ( ( ) {
final newMarker = Marker (
markerId: MarkerId ( _lastMapPosition . toString ( ) ) ,
position: _lastMapPosition ,
infoWindow: InfoWindow ( title: ' New Place ' ) ,
draggable: true ,
icon: BitmapDescriptor . defaultMarkerWithHue ( BitmapDescriptor . hueGreen ) ,
) ;
_markers . add ( newMarker ) ;
_pendingMarker = newMarker ;
} ) ;
}
}
Future < void > _confirmAddPlace ( BuildContext context ) async {
Future < void > _confirmAddPlace ( BuildContext context ) async {
@ -298,22 +217,21 @@ class PlaceMapState extends State<PlaceMap> {
}
}
}
}
void _cancelAddPlace ( ) {
Future < Marker > _createPlaceMarker ( BuildContext context , Place place ) async {
if ( _pendingMarker ! = null ) {
final marker = Marker (
setState ( ( ) {
markerId: MarkerId ( place . latLng . toString ( ) ) ,
_markers . remove ( _pendingMarker ) ;
position: place . latLng ,
_pendingMarker = null ;
infoWindow: InfoWindow (
} ) ;
title: place . name ,
}
snippet: ' ${ place . starRating } Star Rating ' ,
}
onTap: ( ) = > _pushPlaceDetailsScreen ( place ) ,
) ,
void _onToggleMapTypePressed ( ) {
icon: await _getPlaceMarkerIcon ( context , place . category ) ,
final nextType =
visible: place . category = =
MapType . values [ ( _currentMapType . index + 1 ) % MapType . values . length ] ;
Provider . of < AppState > ( context , listen: false ) . selectedCategory ,
) ;
setState ( ( ) {
_markedPlaces [ marker ] = place ;
_currentMapType = nextType ;
return marker ;
} ) ;
}
}
Future < void > _maybeUpdateMapConfiguration ( ) async {
Future < void > _maybeUpdateMapConfiguration ( ) async {
@ -351,52 +269,222 @@ class PlaceMapState extends State<PlaceMap> {
}
}
}
}
Future < void > _onAddPlacePressed ( ) async {
setState ( ( ) {
final newMarker = Marker (
markerId: MarkerId ( _lastMapPosition . toString ( ) ) ,
position: _lastMapPosition ,
infoWindow: InfoWindow ( title: ' New Place ' ) ,
draggable: true ,
icon: BitmapDescriptor . defaultMarkerWithHue ( BitmapDescriptor . hueGreen ) ,
) ;
_markers . add ( newMarker ) ;
_pendingMarker = newMarker ;
} ) ;
}
void _onPlaceChanged ( Place value ) {
/ / Replace the place with the modified version .
final newPlaces =
List < Place > . from ( Provider . of < AppState > ( context , listen: false ) . places ) ;
final index = newPlaces . indexWhere ( ( place ) = > place . id = = value . id ) ;
newPlaces [ index ] = value ;
_updateExistingPlaceMarker ( place: value ) ;
/ / Manually update our map configuration here since our map is already
/ / updated with the new marker . Otherwise , the map would be reconfigured
/ / in the main build method due to a modified AppState .
_configuration = MapConfiguration (
places: newPlaces ,
selectedCategory:
Provider . of < AppState > ( context , listen: false ) . selectedCategory ,
) ;
Provider . of < AppState > ( context , listen: false ) . setPlaces ( newPlaces ) ;
}
void _onToggleMapTypePressed ( ) {
final nextType =
MapType . values [ ( _currentMapType . index + 1 ) % MapType . values . length ] ;
setState ( ( ) {
_currentMapType = nextType ;
} ) ;
}
void _pushPlaceDetailsScreen ( Place place ) {
assert ( place ! = null ) ;
Navigator . push < void > (
context ,
MaterialPageRoute ( builder: ( context ) {
return PlaceDetails (
place: place ,
onChanged: ( value ) = > _onPlaceChanged ( value ) ,
) ;
} ) ,
) ;
}
Future < void > _showPlacesForSelectedCategory ( PlaceCategory category ) async {
setState ( ( ) {
for ( var marker in List . of ( _markedPlaces . keys ) ) {
final place = _markedPlaces [ marker ] ;
final updatedMarker = marker . copyWith (
visibleParam: place . category = = category ,
) ;
_updateMarker (
marker: marker ,
updatedMarker: updatedMarker ,
place: place ,
) ;
}
} ) ;
await _zoomToFitPlaces ( _getPlacesForCategory (
category ,
_markedPlaces . values . toList ( ) ,
) ) ;
}
Future < void > _switchSelectedCategory ( PlaceCategory category ) async {
Provider . of < AppState > ( context , listen: false ) . setSelectedCategory ( category ) ;
await _showPlacesForSelectedCategory ( category ) ;
}
void _updateExistingPlaceMarker ( { @ required Place place } ) {
var marker = _markedPlaces . keys
. singleWhere ( ( value ) = > _markedPlaces [ value ] . id = = place . id ) ;
setState ( ( ) {
final updatedMarker = marker . copyWith (
infoWindowParam: InfoWindow (
title: place . name ,
snippet:
place . starRating ! = 0 ? ' ${ place . starRating } Star Rating ' : null ,
) ,
) ;
_updateMarker ( marker: marker , updatedMarker: updatedMarker , place: place ) ;
} ) ;
}
void _updateMarker ( {
@ required Marker marker ,
@ required Marker updatedMarker ,
@ required Place place ,
} ) {
_markers . remove ( marker ) ;
_markedPlaces . remove ( marker ) ;
_markers . add ( updatedMarker ) ;
_markedPlaces [ updatedMarker ] = place ;
}
Future < void > _zoomToFitPlaces ( List < Place > places ) async {
var controller = await mapController . future ;
/ / Default min / max values to latitude and longitude of center .
var minLat = widget . center . latitude ;
var maxLat = widget . center . latitude ;
var minLong = widget . center . longitude ;
var maxLong = widget . center . longitude ;
for ( var place in places ) {
minLat = min ( minLat , place . latitude ) ;
maxLat = max ( maxLat , place . latitude ) ;
minLong = min ( minLong , place . longitude ) ;
maxLong = max ( maxLong , place . longitude ) ;
}
await controller . animateCamera (
CameraUpdate . newLatLngBounds (
LatLngBounds (
southwest: LatLng ( minLat , minLong ) ,
northeast: LatLng ( maxLat , maxLong ) ,
) ,
48.0 ,
) ,
) ;
}
static Future < BitmapDescriptor > _getPlaceMarkerIcon (
BuildContext context , PlaceCategory category ) async {
switch ( category ) {
case PlaceCategory . favorite:
return BitmapDescriptor . fromAssetImage (
createLocalImageConfiguration ( context ) , ' assets/heart.png ' ) ;
break ;
case PlaceCategory . visited:
return BitmapDescriptor . fromAssetImage (
createLocalImageConfiguration ( context ) , ' assets/visited.png ' ) ;
break ;
case PlaceCategory . wantToGo:
default :
return BitmapDescriptor . defaultMarker ;
}
}
static List < Place > _getPlacesForCategory (
PlaceCategory category , List < Place > places ) {
return places . where ( ( place ) = > place . category = = category ) . toList ( ) ;
}
}
class _AddPlaceButtonBar extends StatelessWidget {
final bool visible ;
final VoidCallback onSavePressed ;
final VoidCallback onCancelPressed ;
const _AddPlaceButtonBar ( {
Key key ,
@ required this . visible ,
@ required this . onSavePressed ,
@ required this . onCancelPressed ,
} ) : assert ( visible ! = null ) ,
assert ( onSavePressed ! = null ) ,
assert ( onCancelPressed ! = null ) ,
super ( key: key ) ;
@ override
@ override
Widget build ( BuildContext context ) {
Widget build ( BuildContext context ) {
_maybeUpdateMapConfiguration ( ) ;
return Visibility (
var state = Provider . of < AppState > ( context ) ;
visible: visible ,
child: Container (
return Builder ( builder: ( context ) {
padding: const EdgeInsets . fromLTRB ( 0.0 , 0.0 , 0.0 , 14.0 ) ,
/ / We need this additional builder here so that we can pass its context to
alignment: Alignment . bottomCenter ,
/ / _AddPlaceButtonBar ' s onSavePressed callback. This callback shows a
child: ButtonBar (
/ / SnackBar and to do this , we need a build context that has Scaffold as
alignment: MainAxisAlignment . center ,
/ / an ancestor .
children: [
return Center (
RaisedButton (
child: Stack (
color: Colors . blue ,
children: < Widget > [
child: const Text (
GoogleMap (
' Save ' ,
onMapCreated: onMapCreated ,
style: TextStyle ( color: Colors . white , fontSize: 16.0 ) ,
initialCameraPosition: CameraPosition (
target: widget . center ,
zoom: 11.0 ,
) ,
) ,
mapType: _currentMapType ,
onPressed: onSavePressed ,
markers: _markers ,
onCameraMove: ( position ) = > _lastMapPosition = position . target ,
) ,
_CategoryButtonBar (
selectedPlaceCategory: state . selectedCategory ,
visible: _pendingMarker = = null ,
onChanged: _switchSelectedCategory ,
) ,
_AddPlaceButtonBar (
visible: _pendingMarker ! = null ,
onSavePressed: ( ) = > _confirmAddPlace ( context ) ,
onCancelPressed: _cancelAddPlace ,
) ,
) ,
_MapFabs (
RaisedButton (
visible: _pendingMarker = = null ,
color: Colors . red ,
onAddPlacePressed: _onAddPlacePressed ,
child: const Text (
onToggleMapTypePressed: _onToggleMapTypePressed ,
' Cancel ' ,
style: TextStyle ( color: Colors . white , fontSize: 16.0 ) ,
) ,
onPressed: onCancelPressed ,
) ,
) ,
] ,
] ,
) ,
) ,
) ;
) ,
} ) ;
);
}
}
}
}
class _CategoryButtonBar extends StatelessWidget {
class _CategoryButtonBar extends StatelessWidget {
final PlaceCategory selectedPlaceCategory ;
final bool visible ;
final ValueChanged < PlaceCategory > onChanged ;
const _CategoryButtonBar ( {
const _CategoryButtonBar ( {
Key key ,
Key key ,
@ required this . selectedPlaceCategory ,
@ required this . selectedPlaceCategory ,
@ -407,10 +495,6 @@ class _CategoryButtonBar extends StatelessWidget {
assert ( onChanged ! = null ) ,
assert ( onChanged ! = null ) ,
super ( key: key ) ;
super ( key: key ) ;
final PlaceCategory selectedPlaceCategory ;
final bool visible ;
final ValueChanged < PlaceCategory > onChanged ;
@ override
@ override
Widget build ( BuildContext context ) {
Widget build ( BuildContext context ) {
return Visibility (
return Visibility (
@ -420,7 +504,7 @@ class _CategoryButtonBar extends StatelessWidget {
alignment: Alignment . bottomCenter ,
alignment: Alignment . bottomCenter ,
child: ButtonBar (
child: ButtonBar (
alignment: MainAxisAlignment . center ,
alignment: MainAxisAlignment . center ,
children: < Widget > [
children: [
RaisedButton (
RaisedButton (
color: selectedPlaceCategory = = PlaceCategory . favorite
color: selectedPlaceCategory = = PlaceCategory . favorite
? Colors . green [ 700 ]
? Colors . green [ 700 ]
@ -458,55 +542,11 @@ class _CategoryButtonBar extends StatelessWidget {
}
}
}
}
class _AddPlaceButtonBar extends StatelessWidget {
class _MapFabs extends StatelessWidget {
const _AddPlaceButtonBar ( {
Key key ,
@ required this . visible ,
@ required this . onSavePressed ,
@ required this . onCancelPressed ,
} ) : assert ( visible ! = null ) ,
assert ( onSavePressed ! = null ) ,
assert ( onCancelPressed ! = null ) ,
super ( key: key ) ;
final bool visible ;
final bool visible ;
final VoidCallback onSavePressed ;
final VoidCallback onAddPlacePressed ;
final VoidCallback onCancelPressed ;
final VoidCallback onToggleMapTypePressed ;
@ override
Widget build ( BuildContext context ) {
return Visibility (
visible: visible ,
child: Container (
padding: const EdgeInsets . fromLTRB ( 0.0 , 0.0 , 0.0 , 14.0 ) ,
alignment: Alignment . bottomCenter ,
child: ButtonBar (
alignment: MainAxisAlignment . center ,
children: < Widget > [
RaisedButton (
color: Colors . blue ,
child: const Text (
' Save ' ,
style: TextStyle ( color: Colors . white , fontSize: 16.0 ) ,
) ,
onPressed: onSavePressed ,
) ,
RaisedButton (
color: Colors . red ,
child: const Text (
' Cancel ' ,
style: TextStyle ( color: Colors . white , fontSize: 16.0 ) ,
) ,
onPressed: onCancelPressed ,
) ,
] ,
) ,
) ,
) ;
}
}
class _MapFabs extends StatelessWidget {
const _MapFabs ( {
const _MapFabs ( {
Key key ,
Key key ,
@ required this . visible ,
@ required this . visible ,
@ -517,10 +557,6 @@ class _MapFabs extends StatelessWidget {
assert ( onToggleMapTypePressed ! = null ) ,
assert ( onToggleMapTypePressed ! = null ) ,
super ( key: key ) ;
super ( key: key ) ;
final bool visible ;
final VoidCallback onAddPlacePressed ;
final VoidCallback onToggleMapTypePressed ;
@ override
@ override
Widget build ( BuildContext context ) {
Widget build ( BuildContext context ) {
return Container (
return Container (
@ -529,7 +565,7 @@ class _MapFabs extends StatelessWidget {
child: Visibility (
child: Visibility (
visible: visible ,
visible: visible ,
child: Column (
child: Column (
children: < Widget > [
children: [
FloatingActionButton (
FloatingActionButton (
heroTag: ' add_place_button ' ,
heroTag: ' add_place_button ' ,
onPressed: onAddPlacePressed ,
onPressed: onAddPlacePressed ,
@ -552,39 +588,3 @@ class _MapFabs extends StatelessWidget {
) ;
) ;
}
}
}
}
class MapConfiguration {
const MapConfiguration ( {
@ required this . places ,
@ required this . selectedCategory ,
} ) : assert ( places ! = null ) ,
assert ( selectedCategory ! = null ) ;
final List < Place > places ;
final PlaceCategory selectedCategory ;
@ override
int get hashCode = > places . hashCode ^ selectedCategory . hashCode ;
@ override
bool operator = = ( Object other ) {
if ( identical ( this , other ) ) {
return true ;
}
if ( other . runtimeType ! = runtimeType ) {
return false ;
}
return other is MapConfiguration & &
other . places = = places & &
other . selectedCategory = = selectedCategory ;
}
static MapConfiguration of ( AppState appState ) {
return MapConfiguration (
places: appState . places ,
selectedCategory: appState . selectedCategory ,
) ;
}
}