From 972fcd26a68a0e71edfc0931ae7dc8ab8a0d74f2 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Fri, 10 May 2024 17:14:57 -0400 Subject: [PATCH 01/11] initial implementation still missing actual map --- packages/app/lib/locales/app_en.arb | 14 ++ packages/app/lib/ui/screens/sighting.dart | 17 ++ .../app/lib/ui/widgets/location_field.dart | 149 ++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 packages/app/lib/ui/widgets/location_field.dart diff --git a/packages/app/lib/locales/app_en.arb b/packages/app/lib/locales/app_en.arb index c9d6bd40..0bdd65e1 100644 --- a/packages/app/lib/locales/app_en.arb +++ b/packages/app/lib/locales/app_en.arb @@ -71,6 +71,20 @@ "sightingDeleteAlertConfirm": "Delete", "sightingDeleteAlertTitle": "Delete Sighting", "sightingDeleteConfirmation": "Sighting deleted.", + "sightingLocationFieldTitle": "Location", + "sightingLocationFieldSave": "Save", + "sightingLocationFieldCancel": "Cancel", + "sightingLocationFieldCoordinates": "Coordinates: Lat {latitude}, Lon {longitude}", + "@sightingLocationFieldCoordinates": { + "placeholders": { + "latitude": { + "type": "double" + }, + "longitude": { + "type": "double" + } + } + }, "sightingScreenTitle": "Sighting", "sightingUnspecified": "Unknown species", "speciesCardTitle": "Species", diff --git a/packages/app/lib/ui/screens/sighting.dart b/packages/app/lib/ui/screens/sighting.dart index e31d86c0..ed660b00 100644 --- a/packages/app/lib/ui/screens/sighting.dart +++ b/packages/app/lib/ui/screens/sighting.dart @@ -1,5 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +import 'package:app/ui/widgets/location_field.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; @@ -159,6 +160,18 @@ class _SightingProfileState extends State { setState(() {}); } + void _updateLocation( + {required double latitude, required double longitude}) async { + if (sighting.latitude == latitude && sighting.longitude == longitude) { + // Nothing has changed + return; + } + + await sighting.update(latitude: latitude, longitude: longitude); + + setState(() {}); + } + @override Widget build(BuildContext context) { final imagePaths = @@ -185,6 +198,10 @@ class _SightingProfileState extends State { EditableTextField(sighting.comment, title: AppLocalizations.of(context)!.noteCardTitle, onUpdate: _updateComment), + LocationField( + latitude: sighting.latitude, + longitude: sighting.longitude, + onUpdate: _updateLocation), ]), ); } diff --git a/packages/app/lib/ui/widgets/location_field.dart b/packages/app/lib/ui/widgets/location_field.dart new file mode 100644 index 00000000..de3bce4c --- /dev/null +++ b/packages/app/lib/ui/widgets/location_field.dart @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import 'dart:math'; + +import 'package:app/ui/widgets/card.dart'; +import 'package:app/ui/widgets/card_action_button.dart'; +import 'package:app/ui/widgets/card_header.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +typedef OnUpdate = void Function( + {required double latitude, required double longitude}); + +class LocationField extends StatefulWidget { + final double latitude; + final double longitude; + + final OnUpdate onUpdate; + + const LocationField( + {super.key, + required this.longitude, + required this.latitude, + required this.onUpdate}); + + @override + State createState() => _LocationFieldState(); +} + +enum InputMode { read, edit } + +class _LocationFieldState extends State { + InputMode _inputMode = InputMode.read; + + late double _lat; + late double _lon; + + @override + void initState() { + _lat = widget.latitude; + _lon = widget.longitude; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final t = AppLocalizations.of(context)!; + + return MeliCard( + child: Column( + children: [ + MeliCardHeader( + title: t.sightingLocationFieldTitle, + icon: _inputMode == InputMode.read + ? CardActionButton( + icon: const Icon(Icons.edit_outlined), + onPressed: _handleStartEditMode) + : null), + Container( + padding: + const EdgeInsets.symmetric(vertical: 10.0, horizontal: 6.0), + alignment: AlignmentDirectional.centerStart, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _renderMap(), + Text(t.sightingLocationFieldCoordinates(_lat, _lon), + style: const TextStyle( + fontSize: 16.0, + )), + if (_inputMode == InputMode.edit) ...[ + const SizedBox(height: 12), + Row(children: [ + OverflowBar( + spacing: 12, + overflowAlignment: OverflowBarAlignment.start, + children: [ + FilledButton( + onPressed: _handleSave, + child: Text( + t.sightingLocationFieldSave, + )), + OutlinedButton( + onPressed: _handleCancel, + child: Text( + t.sightingLocationFieldCancel, + )) + ], + ) + ]) + ] + ], + )) + ], + )); + } + + Widget _renderMap() { + // TODO: replace with actual map + return Container( + height: 200, + alignment: Alignment.center, + child: _inputMode == InputMode.read + ? const Text("🚧 There should be a map here 🚧") + : OutlinedButton( + onPressed: () { + final r = Random(); + + double newLat = r.nextInt(181).toDouble(); + double newLon = r.nextInt(91).toDouble(); + + setState(() { + _lon = newLon; + _lat = newLat; + }); + }, + child: const Text("Simulate location change")), + ); + } + + void _handleSave() { + // Do nothing if coordinates have not changed at all + if (_lat == widget.latitude && _lon == widget.longitude) { + _handleCancel(); + return; + } + + setState(() { + _inputMode = InputMode.read; + widget.onUpdate(latitude: _lat, longitude: _lon); + }); + } + + void _handleCancel() { + setState(() { + _inputMode = InputMode.read; + + // Reset coordinates to initial values + _lat = widget.latitude; + _lon = widget.longitude; + }); + } + + void _handleStartEditMode() { + setState(() { + _inputMode = InputMode.edit; + }); + } +} From 64a216a73352372bdaf03627c4f632b57685a9f5 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Fri, 17 May 2024 19:01:45 -0400 Subject: [PATCH 02/11] implementation with map --- .../app/lib/ui/widgets/location_field.dart | 110 ++++++++++++++---- packages/app/pubspec.lock | 27 +++++ packages/app/pubspec.yaml | 4 + 3 files changed, 117 insertions(+), 24 deletions(-) diff --git a/packages/app/lib/ui/widgets/location_field.dart b/packages/app/lib/ui/widgets/location_field.dart index de3bce4c..3e5321e1 100644 --- a/packages/app/lib/ui/widgets/location_field.dart +++ b/packages/app/lib/ui/widgets/location_field.dart @@ -1,12 +1,13 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import 'dart:math'; - +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:app/ui/widgets/card.dart'; import 'package:app/ui/widgets/card_action_button.dart'; import 'package:app/ui/widgets/card_header.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; typedef OnUpdate = void Function( {required double latitude, required double longitude}); @@ -29,12 +30,19 @@ class LocationField extends StatefulWidget { enum InputMode { read, edit } +double DEFAULT_ZOOM = 10; +String CURRENT_FEATURE_ID = 'current'; +String TARGET_FEATURE_ID = 'target'; + class _LocationFieldState extends State { InputMode _inputMode = InputMode.read; late double _lat; late double _lon; + MaplibreMapController? _mapController; + Circle? _currentMapMarker; + @override void initState() { _lat = widget.latitude; @@ -64,6 +72,9 @@ class _LocationFieldState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ _renderMap(), + const SizedBox( + height: 20, + ), Text(t.sightingLocationFieldCoordinates(_lat, _lon), style: const TextStyle( fontSize: 16.0, @@ -96,29 +107,65 @@ class _LocationFieldState extends State { } Widget _renderMap() { - // TODO: replace with actual map + bool enableInteractions = _inputMode == InputMode.edit; + return Container( - height: 200, - alignment: Alignment.center, - child: _inputMode == InputMode.read - ? const Text("🚧 There should be a map here 🚧") - : OutlinedButton( - onPressed: () { - final r = Random(); - - double newLat = r.nextInt(181).toDouble(); - double newLon = r.nextInt(91).toDouble(); - - setState(() { - _lon = newLon; - _lat = newLat; - }); - }, - child: const Text("Simulate location change")), - ); + alignment: Alignment.center, + height: 200, + child: MaplibreMap( + initialCameraPosition: CameraPosition( + target: LatLng(widget.latitude, widget.longitude), + zoom: DEFAULT_ZOOM), + scrollGesturesEnabled: enableInteractions, + dragEnabled: enableInteractions, + zoomGesturesEnabled: enableInteractions, + trackCameraPosition: enableInteractions, + compassEnabled: false, + rotateGesturesEnabled: false, + onMapCreated: (controller) { + _mapController = controller; + }, + gestureRecognizers: >{ + Factory( + () => EagerGestureRecognizer()), + }, + onStyleLoadedCallback: () async { + _currentMapMarker = await _mapController!.addCircle(CircleOptions( + circleRadius: 8, + circleColor: Colors.blue.toHexStringRGB(), + geometry: LatLng(widget.latitude, widget.longitude))); + }, + onMapClick: (_, latLng) async { + if (_inputMode == InputMode.read) return; + if (_currentMapMarker == null) return; + + var existingNextMarker = _mapController!.circles.where((c) { + return c != _currentMapMarker; + }).firstOrNull; + + if (existingNextMarker == null) { + await _mapController!.addCircle(CircleOptions( + circleRadius: 8, + circleColor: Colors.blue.toHexStringRGB(), + geometry: LatLng(latLng.latitude, latLng.longitude))); + + await _mapController!.updateCircle(_currentMapMarker!, + const CircleOptions(circleOpacity: 0.4)); + } else { + await _mapController!.updateCircle( + existingNextMarker, CircleOptions(geometry: latLng)); + } + + _mapController!.animateCamera(CameraUpdate.newLatLng(latLng)); + + setState(() { + _lon = latLng.longitude; + _lat = latLng.latitude; + }); + })); } - void _handleSave() { + void _handleSave() async { // Do nothing if coordinates have not changed at all if (_lat == widget.latitude && _lon == widget.longitude) { _handleCancel(); @@ -129,13 +176,16 @@ class _LocationFieldState extends State { _inputMode = InputMode.read; widget.onUpdate(latitude: _lat, longitude: _lon); }); + + _resetMap(LatLng(_lat, _lon)); } - void _handleCancel() { + void _handleCancel() async { + await _resetMap(LatLng(widget.latitude, widget.longitude)); + setState(() { _inputMode = InputMode.read; - // Reset coordinates to initial values _lat = widget.latitude; _lon = widget.longitude; }); @@ -146,4 +196,16 @@ class _LocationFieldState extends State { _inputMode = InputMode.edit; }); } + + Future _resetMap(LatLng latLng) async { + await _mapController!.updateCircle(_currentMapMarker!, + CircleOptions(geometry: latLng, circleOpacity: 1.0)); + + await _mapController!.removeCircles(_mapController!.circles.where((c) { + return c != _currentMapMarker; + })); + + _mapController! + .moveCamera(CameraUpdate.newLatLngZoom(latLng, DEFAULT_ZOOM)); + } } diff --git a/packages/app/pubspec.lock b/packages/app/pubspec.lock index e44ddc4b..87c8f98f 100644 --- a/packages/app/pubspec.lock +++ b/packages/app/pubspec.lock @@ -693,6 +693,33 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + maplibre_gl: + dependency: "direct main" + description: + path: "." + ref: "0.18.0" + resolved-ref: "9397fae65c533e8ff0828bf62511ca34082763ff" + url: "https://github.com/maplibre/flutter-maplibre-gl.git" + source: git + version: "0.18.0" + maplibre_gl_platform_interface: + dependency: transitive + description: + path: maplibre_gl_platform_interface + ref: "git-release-0.18.0" + resolved-ref: "9397fae65c533e8ff0828bf62511ca34082763ff" + url: "https://github.com/maplibre/flutter-maplibre-gl.git" + source: git + version: "0.18.0" + maplibre_gl_web: + dependency: transitive + description: + path: maplibre_gl_web + ref: "git-release-0.18.0" + resolved-ref: "9397fae65c533e8ff0828bf62511ca34082763ff" + url: "https://github.com/maplibre/flutter-maplibre-gl.git" + source: git + version: "0.18.0" matcher: dependency: transitive description: diff --git a/packages/app/pubspec.yaml b/packages/app/pubspec.yaml index a693c4cf..e006bc0f 100644 --- a/packages/app/pubspec.yaml +++ b/packages/app/pubspec.yaml @@ -27,6 +27,10 @@ dependencies: http: 1.2.1 image_picker: 1.1.2 intl: 0.19.0 + maplibre_gl: + git: + url: https://github.com/maplibre/flutter-maplibre-gl.git + ref: 0.18.0 mime: 1.0.5 p2panda: 0.1.1 p2panda_flutter: 0.1.1 From d32f9c2301abf6f8862e72c9902cbba109e35c89 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Fri, 31 May 2024 12:17:52 -0400 Subject: [PATCH 03/11] use shared widgets --- packages/app/lib/locales/app_en.arb | 2 - .../app/lib/ui/widgets/location_field.dart | 111 ++++++------------ 2 files changed, 38 insertions(+), 75 deletions(-) diff --git a/packages/app/lib/locales/app_en.arb b/packages/app/lib/locales/app_en.arb index 0bdd65e1..b562d180 100644 --- a/packages/app/lib/locales/app_en.arb +++ b/packages/app/lib/locales/app_en.arb @@ -72,8 +72,6 @@ "sightingDeleteAlertTitle": "Delete Sighting", "sightingDeleteConfirmation": "Sighting deleted.", "sightingLocationFieldTitle": "Location", - "sightingLocationFieldSave": "Save", - "sightingLocationFieldCancel": "Cancel", "sightingLocationFieldCoordinates": "Coordinates: Lat {latitude}, Lon {longitude}", "@sightingLocationFieldCoordinates": { "placeholders": { diff --git a/packages/app/lib/ui/widgets/location_field.dart b/packages/app/lib/ui/widgets/location_field.dart index 3e5321e1..b35d0c50 100644 --- a/packages/app/lib/ui/widgets/location_field.dart +++ b/packages/app/lib/ui/widgets/location_field.dart @@ -1,10 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +import 'package:app/ui/widgets/editable_card.dart'; +import 'package:app/ui/widgets/save_cancel_buttons.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; -import 'package:app/ui/widgets/card.dart'; -import 'package:app/ui/widgets/card_action_button.dart'; -import 'package:app/ui/widgets/card_header.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -28,18 +27,16 @@ class LocationField extends StatefulWidget { State createState() => _LocationFieldState(); } -enum InputMode { read, edit } - double DEFAULT_ZOOM = 10; String CURRENT_FEATURE_ID = 'current'; String TARGET_FEATURE_ID = 'target'; class _LocationFieldState extends State { - InputMode _inputMode = InputMode.read; - late double _lat; late double _lon; + bool _isEditMode = false; + MaplibreMapController? _mapController; Circle? _currentMapMarker; @@ -54,61 +51,36 @@ class _LocationFieldState extends State { Widget build(BuildContext context) { final t = AppLocalizations.of(context)!; - return MeliCard( - child: Column( - children: [ - MeliCardHeader( - title: t.sightingLocationFieldTitle, - icon: _inputMode == InputMode.read - ? CardActionButton( - icon: const Icon(Icons.edit_outlined), - onPressed: _handleStartEditMode) - : null), - Container( - padding: - const EdgeInsets.symmetric(vertical: 10.0, horizontal: 6.0), - alignment: AlignmentDirectional.centerStart, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _renderMap(), - const SizedBox( - height: 20, - ), - Text(t.sightingLocationFieldCoordinates(_lat, _lon), - style: const TextStyle( - fontSize: 16.0, - )), - if (_inputMode == InputMode.edit) ...[ - const SizedBox(height: 12), - Row(children: [ - OverflowBar( - spacing: 12, - overflowAlignment: OverflowBarAlignment.start, - children: [ - FilledButton( - onPressed: _handleSave, - child: Text( - t.sightingLocationFieldSave, - )), - OutlinedButton( - onPressed: _handleCancel, - child: Text( - t.sightingLocationFieldCancel, - )) - ], - ) - ]) - ] - ], - )) - ], - )); + return EditableCard( + title: t.sightingLocationFieldTitle, + isEditMode: _isEditMode, + onChanged: _isEditMode + ? _handleCancel + : () { + setState(() { + _isEditMode = true; + }); + }, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + _renderMap(), + const SizedBox( + height: 20, + ), + Text(t.sightingLocationFieldCoordinates(_lat, _lon), + style: const TextStyle( + fontSize: 16.0, + )), + if (_isEditMode) ...[ + Padding( + padding: const EdgeInsets.only(top: 10.0), + child: SaveCancelButtons( + handleCancel: _handleCancel, handleSave: _handleSave)), + ] + ]), + ); } Widget _renderMap() { - bool enableInteractions = _inputMode == InputMode.edit; - return Container( alignment: Alignment.center, height: 200, @@ -116,10 +88,10 @@ class _LocationFieldState extends State { initialCameraPosition: CameraPosition( target: LatLng(widget.latitude, widget.longitude), zoom: DEFAULT_ZOOM), - scrollGesturesEnabled: enableInteractions, - dragEnabled: enableInteractions, - zoomGesturesEnabled: enableInteractions, - trackCameraPosition: enableInteractions, + scrollGesturesEnabled: _isEditMode, + dragEnabled: _isEditMode, + zoomGesturesEnabled: _isEditMode, + trackCameraPosition: _isEditMode, compassEnabled: false, rotateGesturesEnabled: false, onMapCreated: (controller) { @@ -136,7 +108,7 @@ class _LocationFieldState extends State { geometry: LatLng(widget.latitude, widget.longitude))); }, onMapClick: (_, latLng) async { - if (_inputMode == InputMode.read) return; + if (!_isEditMode) return; if (_currentMapMarker == null) return; var existingNextMarker = _mapController!.circles.where((c) { @@ -173,7 +145,7 @@ class _LocationFieldState extends State { } setState(() { - _inputMode = InputMode.read; + _isEditMode = false; widget.onUpdate(latitude: _lat, longitude: _lon); }); @@ -184,19 +156,12 @@ class _LocationFieldState extends State { await _resetMap(LatLng(widget.latitude, widget.longitude)); setState(() { - _inputMode = InputMode.read; - + _isEditMode = false; _lat = widget.latitude; _lon = widget.longitude; }); } - void _handleStartEditMode() { - setState(() { - _inputMode = InputMode.edit; - }); - } - Future _resetMap(LatLng latLng) async { await _mapController!.updateCircle(_currentMapMarker!, CircleOptions(geometry: latLng, circleOpacity: 1.0)); From 332066d424f3c2ec14854eac5d75938af51ccfa5 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Fri, 31 May 2024 13:37:00 -0400 Subject: [PATCH 04/11] adjust diplay of coordinates --- packages/app/lib/locales/app_en.arb | 24 +++++++++++++++++++ .../app/lib/ui/widgets/location_field.dart | 11 ++++----- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/packages/app/lib/locales/app_en.arb b/packages/app/lib/locales/app_en.arb index b562d180..2c169a21 100644 --- a/packages/app/lib/locales/app_en.arb +++ b/packages/app/lib/locales/app_en.arb @@ -72,6 +72,30 @@ "sightingDeleteAlertTitle": "Delete Sighting", "sightingDeleteConfirmation": "Sighting deleted.", "sightingLocationFieldTitle": "Location", + "sightingLocationLatitude": "Latitude: {latitude}", + "@sightingLocationLatitude": { + "placeholders": { + "latitude": { + "type": "double", + "format": "decimalPattern", + "optionalParameters": { + "decimalDigits": 4 + } + } + } + }, + "sightingLocationLongitude": "Longitude: {longitude}", + "@sightingLocationLongitude": { + "placeholders": { + "longitude": { + "type": "double", + "format": "decimalPattern", + "optionalParameters": { + "decimalDigits": 4 + } + } + } + }, "sightingLocationFieldCoordinates": "Coordinates: Lat {latitude}, Lon {longitude}", "@sightingLocationFieldCoordinates": { "placeholders": { diff --git a/packages/app/lib/ui/widgets/location_field.dart b/packages/app/lib/ui/widgets/location_field.dart index b35d0c50..5668b77c 100644 --- a/packages/app/lib/ui/widgets/location_field.dart +++ b/packages/app/lib/ui/widgets/location_field.dart @@ -66,16 +66,15 @@ class _LocationFieldState extends State { const SizedBox( height: 20, ), - Text(t.sightingLocationFieldCoordinates(_lat, _lon), - style: const TextStyle( - fontSize: 16.0, - )), - if (_isEditMode) ...[ + Text(t.sightingLocationLongitude(_lon), + style: Theme.of(context).textTheme.bodyLarge), + Text(t.sightingLocationLatitude(_lat), + style: Theme.of(context).textTheme.bodyLarge), + if (_isEditMode) Padding( padding: const EdgeInsets.only(top: 10.0), child: SaveCancelButtons( handleCancel: _handleCancel, handleSave: _handleSave)), - ] ]), ); } From c6c1c353df765990df00014ab9b198f37393d1de Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Fri, 31 May 2024 13:37:45 -0400 Subject: [PATCH 05/11] animate camera when resetting map --- packages/app/lib/ui/widgets/location_field.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/lib/ui/widgets/location_field.dart b/packages/app/lib/ui/widgets/location_field.dart index 5668b77c..ab6d6280 100644 --- a/packages/app/lib/ui/widgets/location_field.dart +++ b/packages/app/lib/ui/widgets/location_field.dart @@ -170,6 +170,6 @@ class _LocationFieldState extends State { })); _mapController! - .moveCamera(CameraUpdate.newLatLngZoom(latLng, DEFAULT_ZOOM)); + .animateCamera(CameraUpdate.newLatLngZoom(latLng, DEFAULT_ZOOM)); } } From af8d6ffbb3c23e77967c60a27e4addaddff4ba69 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Fri, 31 May 2024 14:01:23 -0400 Subject: [PATCH 06/11] update marker appearance --- .../app/lib/ui/widgets/location_field.dart | 75 ++++++++++++------- 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/packages/app/lib/ui/widgets/location_field.dart b/packages/app/lib/ui/widgets/location_field.dart index ab6d6280..579c2a8b 100644 --- a/packages/app/lib/ui/widgets/location_field.dart +++ b/packages/app/lib/ui/widgets/location_field.dart @@ -1,5 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +import 'package:app/ui/colors.dart'; import 'package:app/ui/widgets/editable_card.dart'; import 'package:app/ui/widgets/save_cancel_buttons.dart'; import 'package:flutter/foundation.dart'; @@ -27,23 +28,27 @@ class LocationField extends StatefulWidget { State createState() => _LocationFieldState(); } -double DEFAULT_ZOOM = 10; +double DEFAULT_ZOOM = 8; String CURRENT_FEATURE_ID = 'current'; String TARGET_FEATURE_ID = 'target'; class _LocationFieldState extends State { + // The latitude value displayed for the marker at the center of the map late double _lat; - late double _lon; + // The longitude value displayed for the marker at the center of the map + late double _lng; bool _isEditMode = false; MaplibreMapController? _mapController; - Circle? _currentMapMarker; + + // The marker that uses the saved coordinates of the sighting + Circle? _existingMapMarker; @override void initState() { _lat = widget.latitude; - _lon = widget.longitude; + _lng = widget.longitude; super.initState(); } @@ -66,7 +71,7 @@ class _LocationFieldState extends State { const SizedBox( height: 20, ), - Text(t.sightingLocationLongitude(_lon), + Text(t.sightingLocationLongitude(_lng), style: Theme.of(context).textTheme.bodyLarge), Text(t.sightingLocationLatitude(_lat), style: Theme.of(context).textTheme.bodyLarge), @@ -101,36 +106,37 @@ class _LocationFieldState extends State { () => EagerGestureRecognizer()), }, onStyleLoadedCallback: () async { - _currentMapMarker = await _mapController!.addCircle(CircleOptions( - circleRadius: 8, - circleColor: Colors.blue.toHexStringRGB(), - geometry: LatLng(widget.latitude, widget.longitude))); + _existingMapMarker = await _mapController!.addCircle( + createMapCircle(lat: widget.latitude, lng: widget.longitude)); }, onMapClick: (_, latLng) async { if (!_isEditMode) return; - if (_currentMapMarker == null) return; + if (_existingMapMarker == null) return; - var existingNextMarker = _mapController!.circles.where((c) { - return c != _currentMapMarker; + var nextMarker = _mapController!.circles.where((c) { + return c != _existingMapMarker; }).firstOrNull; - if (existingNextMarker == null) { - await _mapController!.addCircle(CircleOptions( - circleRadius: 8, - circleColor: Colors.blue.toHexStringRGB(), - geometry: LatLng(latLng.latitude, latLng.longitude))); + // Add the "next" marker and update the visuals of the existing marker + if (nextMarker == null) { + await _mapController!.addCircle(createMapCircle( + lat: latLng.latitude, lng: latLng.longitude)); - await _mapController!.updateCircle(_currentMapMarker!, - const CircleOptions(circleOpacity: 0.4)); - } else { await _mapController!.updateCircle( - existingNextMarker, CircleOptions(geometry: latLng)); + _existingMapMarker!, + const CircleOptions( + circleOpacity: 0.5, + circleStrokeOpacity: 0.5, + )); + } else { + await _mapController! + .updateCircle(nextMarker, CircleOptions(geometry: latLng)); } _mapController!.animateCamera(CameraUpdate.newLatLng(latLng)); setState(() { - _lon = latLng.longitude; + _lng = latLng.longitude; _lat = latLng.latitude; }); })); @@ -138,17 +144,17 @@ class _LocationFieldState extends State { void _handleSave() async { // Do nothing if coordinates have not changed at all - if (_lat == widget.latitude && _lon == widget.longitude) { + if (_lat == widget.latitude && _lng == widget.longitude) { _handleCancel(); return; } setState(() { _isEditMode = false; - widget.onUpdate(latitude: _lat, longitude: _lon); + widget.onUpdate(latitude: _lat, longitude: _lng); }); - _resetMap(LatLng(_lat, _lon)); + _resetMap(LatLng(_lat, _lng)); } void _handleCancel() async { @@ -157,19 +163,30 @@ class _LocationFieldState extends State { setState(() { _isEditMode = false; _lat = widget.latitude; - _lon = widget.longitude; + _lng = widget.longitude; }); } Future _resetMap(LatLng latLng) async { - await _mapController!.updateCircle(_currentMapMarker!, - CircleOptions(geometry: latLng, circleOpacity: 1.0)); + await _mapController!.updateCircle( + _existingMapMarker!, + CircleOptions( + geometry: latLng, circleOpacity: 1.0, circleStrokeOpacity: 1.0)); await _mapController!.removeCircles(_mapController!.circles.where((c) { - return c != _currentMapMarker; + return c != _existingMapMarker; })); _mapController! .animateCamera(CameraUpdate.newLatLngZoom(latLng, DEFAULT_ZOOM)); } } + +CircleOptions createMapCircle({required double lat, required double lng}) { + return CircleOptions( + circleRadius: 8, + circleColor: MeliColors.magnolia.toHexStringRGB(), + circleStrokeColor: MeliColors.black.toHexStringRGB(), + circleStrokeWidth: 2.0, + geometry: LatLng(lat, lng)); +} From 8fc51c0f52ba196478d50df60299a28bcfbff4f9 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Fri, 31 May 2024 15:31:15 -0400 Subject: [PATCH 07/11] small refactor of coordinate state --- packages/app/lib/ui/screens/sighting.dart | 15 ++- .../app/lib/ui/widgets/location_field.dart | 125 ++++++++++-------- 2 files changed, 82 insertions(+), 58 deletions(-) diff --git a/packages/app/lib/ui/screens/sighting.dart b/packages/app/lib/ui/screens/sighting.dart index ed660b00..779e4207 100644 --- a/packages/app/lib/ui/screens/sighting.dart +++ b/packages/app/lib/ui/screens/sighting.dart @@ -160,14 +160,15 @@ class _SightingProfileState extends State { setState(() {}); } - void _updateLocation( - {required double latitude, required double longitude}) async { - if (sighting.latitude == latitude && sighting.longitude == longitude) { + void _updateLocation(Coordinates coordinates) async { + if (sighting.latitude == coordinates.latitude && + sighting.longitude == coordinates.longitude) { // Nothing has changed return; } - await sighting.update(latitude: latitude, longitude: longitude); + await sighting.update( + latitude: coordinates.latitude, longitude: coordinates.longitude); setState(() {}); } @@ -199,8 +200,10 @@ class _SightingProfileState extends State { title: AppLocalizations.of(context)!.noteCardTitle, onUpdate: _updateComment), LocationField( - latitude: sighting.latitude, - longitude: sighting.longitude, + // Not the best way to check if a position has not been set, but works for now + coordinates: sighting.latitude == 0 && sighting.longitude == 0 + ? null + : (latitude: sighting.latitude, longitude: sighting.longitude), onUpdate: _updateLocation), ]), ); diff --git a/packages/app/lib/ui/widgets/location_field.dart b/packages/app/lib/ui/widgets/location_field.dart index 579c2a8b..d9a05bb8 100644 --- a/packages/app/lib/ui/widgets/location_field.dart +++ b/packages/app/lib/ui/widgets/location_field.dart @@ -9,34 +9,28 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; -typedef OnUpdate = void Function( - {required double latitude, required double longitude}); +typedef OnUpdate = void Function(Coordinates coordinates); +typedef Coordinates = ({double latitude, double longitude}); class LocationField extends StatefulWidget { - final double latitude; - final double longitude; + final Coordinates? coordinates; final OnUpdate onUpdate; const LocationField( - {super.key, - required this.longitude, - required this.latitude, - required this.onUpdate}); + {super.key, required this.coordinates, required this.onUpdate}); @override State createState() => _LocationFieldState(); } -double DEFAULT_ZOOM = 8; -String CURRENT_FEATURE_ID = 'current'; -String TARGET_FEATURE_ID = 'target'; +const Coordinates BRAZIL_CENTROID_COORDINATES = + (latitude: -14.235004, longitude: -51.92528); +const double DEFAULT_ZOOMED_OUT_LEVEL = 1.5; +const double DEFAULT_ZOOMED_IN_LEVEL = 8; class _LocationFieldState extends State { - // The latitude value displayed for the marker at the center of the map - late double _lat; - // The longitude value displayed for the marker at the center of the map - late double _lng; + late Coordinates? _coordinates; bool _isEditMode = false; @@ -47,8 +41,7 @@ class _LocationFieldState extends State { @override void initState() { - _lat = widget.latitude; - _lng = widget.longitude; + _coordinates = widget.coordinates; super.initState(); } @@ -71,10 +64,12 @@ class _LocationFieldState extends State { const SizedBox( height: 20, ), - Text(t.sightingLocationLongitude(_lng), - style: Theme.of(context).textTheme.bodyLarge), - Text(t.sightingLocationLatitude(_lat), - style: Theme.of(context).textTheme.bodyLarge), + if (_coordinates != null) ...[ + Text(t.sightingLocationLongitude(_coordinates!.longitude), + style: Theme.of(context).textTheme.bodyLarge), + Text(t.sightingLocationLatitude(_coordinates!.latitude), + style: Theme.of(context).textTheme.bodyLarge), + ], if (_isEditMode) Padding( padding: const EdgeInsets.only(top: 10.0), @@ -85,13 +80,20 @@ class _LocationFieldState extends State { } Widget _renderMap() { + final initialCameraPosition = _coordinates == null + ? CameraPosition( + zoom: DEFAULT_ZOOMED_OUT_LEVEL, + target: LatLng(BRAZIL_CENTROID_COORDINATES.latitude, + BRAZIL_CENTROID_COORDINATES.longitude)) + : CameraPosition( + target: LatLng(_coordinates!.latitude, _coordinates!.longitude), + zoom: DEFAULT_ZOOMED_IN_LEVEL); + return Container( alignment: Alignment.center, height: 200, child: MaplibreMap( - initialCameraPosition: CameraPosition( - target: LatLng(widget.latitude, widget.longitude), - zoom: DEFAULT_ZOOM), + initialCameraPosition: initialCameraPosition, scrollGesturesEnabled: _isEditMode, dragEnabled: _isEditMode, zoomGesturesEnabled: _isEditMode, @@ -106,28 +108,33 @@ class _LocationFieldState extends State { () => EagerGestureRecognizer()), }, onStyleLoadedCallback: () async { - _existingMapMarker = await _mapController!.addCircle( - createMapCircle(lat: widget.latitude, lng: widget.longitude)); + if (_coordinates != null) { + _existingMapMarker = await _mapController!.addCircle( + createMapCircle( + latitude: _coordinates!.latitude, + longitude: _coordinates!.longitude)); + } }, onMapClick: (_, latLng) async { if (!_isEditMode) return; - if (_existingMapMarker == null) return; - var nextMarker = _mapController!.circles.where((c) { + final nextMarker = _mapController!.circles.where((c) { return c != _existingMapMarker; }).firstOrNull; // Add the "next" marker and update the visuals of the existing marker if (nextMarker == null) { await _mapController!.addCircle(createMapCircle( - lat: latLng.latitude, lng: latLng.longitude)); - - await _mapController!.updateCircle( - _existingMapMarker!, - const CircleOptions( - circleOpacity: 0.5, - circleStrokeOpacity: 0.5, - )); + latitude: latLng.latitude, longitude: latLng.longitude)); + + if (_existingMapMarker != null) { + await _mapController!.updateCircle( + _existingMapMarker!, + const CircleOptions( + circleOpacity: 0.5, + circleStrokeOpacity: 0.5, + )); + } } else { await _mapController! .updateCircle(nextMarker, CircleOptions(geometry: latLng)); @@ -136,57 +143,71 @@ class _LocationFieldState extends State { _mapController!.animateCamera(CameraUpdate.newLatLng(latLng)); setState(() { - _lng = latLng.longitude; - _lat = latLng.latitude; + _coordinates = + (latitude: latLng.latitude, longitude: latLng.longitude); }); })); } void _handleSave() async { // Do nothing if coordinates have not changed at all - if (_lat == widget.latitude && _lng == widget.longitude) { + if (_coordinates == widget.coordinates) { _handleCancel(); return; } setState(() { _isEditMode = false; - widget.onUpdate(latitude: _lat, longitude: _lng); + if (_coordinates != null) { + widget.onUpdate(_coordinates!); + } }); - _resetMap(LatLng(_lat, _lng)); + _resetMap(coordinates: _coordinates!, zoomLevel: DEFAULT_ZOOMED_IN_LEVEL); } void _handleCancel() async { - await _resetMap(LatLng(widget.latitude, widget.longitude)); + if (widget.coordinates == null || _coordinates == null) { + await _resetMap( + coordinates: BRAZIL_CENTROID_COORDINATES, + zoomLevel: DEFAULT_ZOOMED_OUT_LEVEL); + } else { + await _resetMap( + coordinates: widget.coordinates!, zoomLevel: DEFAULT_ZOOMED_IN_LEVEL); + } setState(() { _isEditMode = false; - _lat = widget.latitude; - _lng = widget.longitude; + _coordinates = widget.coordinates; }); } - Future _resetMap(LatLng latLng) async { - await _mapController!.updateCircle( - _existingMapMarker!, - CircleOptions( - geometry: latLng, circleOpacity: 1.0, circleStrokeOpacity: 1.0)); + Future _resetMap( + {required Coordinates coordinates, required double zoomLevel}) async { + final latLng = LatLng(coordinates.latitude, coordinates.longitude); + + if (_existingMapMarker != null) { + await _mapController!.updateCircle( + _existingMapMarker!, + CircleOptions( + geometry: latLng, circleOpacity: 1.0, circleStrokeOpacity: 1.0)); + } await _mapController!.removeCircles(_mapController!.circles.where((c) { return c != _existingMapMarker; })); _mapController! - .animateCamera(CameraUpdate.newLatLngZoom(latLng, DEFAULT_ZOOM)); + .animateCamera(CameraUpdate.newLatLngZoom(latLng, zoomLevel)); } } -CircleOptions createMapCircle({required double lat, required double lng}) { +CircleOptions createMapCircle( + {required double latitude, required double longitude}) { return CircleOptions( circleRadius: 8, circleColor: MeliColors.magnolia.toHexStringRGB(), circleStrokeColor: MeliColors.black.toHexStringRGB(), circleStrokeWidth: 2.0, - geometry: LatLng(lat, lng)); + geometry: LatLng(latitude, longitude)); } From 3eb4071b8e0e5538ce447be079630e1aee034b13 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Fri, 28 Jun 2024 10:01:54 -0400 Subject: [PATCH 08/11] use ActionButtons widget --- packages/app/lib/ui/widgets/location_field.dart | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/app/lib/ui/widgets/location_field.dart b/packages/app/lib/ui/widgets/location_field.dart index d9a05bb8..52ddea6b 100644 --- a/packages/app/lib/ui/widgets/location_field.dart +++ b/packages/app/lib/ui/widgets/location_field.dart @@ -1,8 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import 'package:app/ui/colors.dart'; +import 'package:app/ui/widgets/action_buttons.dart'; import 'package:app/ui/widgets/editable_card.dart'; -import 'package:app/ui/widgets/save_cancel_buttons.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -71,10 +71,7 @@ class _LocationFieldState extends State { style: Theme.of(context).textTheme.bodyLarge), ], if (_isEditMode) - Padding( - padding: const EdgeInsets.only(top: 10.0), - child: SaveCancelButtons( - handleCancel: _handleCancel, handleSave: _handleSave)), + ActionButtons(onCancel: _handleCancel, onAction: _handleSave) ]), ); } From dc93d09755a173b1bd8ca6c99a742d8ab7782107 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Fri, 28 Jun 2024 10:18:11 -0400 Subject: [PATCH 09/11] update maplibre dep to latest --- .../app/lib/ui/widgets/location_field.dart | 4 +-- packages/app/pubspec.lock | 33 +++++++++---------- packages/app/pubspec.yaml | 5 +-- 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/packages/app/lib/ui/widgets/location_field.dart b/packages/app/lib/ui/widgets/location_field.dart index 52ddea6b..8180a4e7 100644 --- a/packages/app/lib/ui/widgets/location_field.dart +++ b/packages/app/lib/ui/widgets/location_field.dart @@ -34,7 +34,7 @@ class _LocationFieldState extends State { bool _isEditMode = false; - MaplibreMapController? _mapController; + MapLibreMapController? _mapController; // The marker that uses the saved coordinates of the sighting Circle? _existingMapMarker; @@ -89,7 +89,7 @@ class _LocationFieldState extends State { return Container( alignment: Alignment.center, height: 200, - child: MaplibreMap( + child: MapLibreMap( initialCameraPosition: initialCameraPosition, scrollGesturesEnabled: _isEditMode, dragEnabled: _isEditMode, diff --git a/packages/app/pubspec.lock b/packages/app/pubspec.lock index 87c8f98f..13c62eb0 100644 --- a/packages/app/pubspec.lock +++ b/packages/app/pubspec.lock @@ -696,30 +696,27 @@ packages: maplibre_gl: dependency: "direct main" description: - path: "." - ref: "0.18.0" - resolved-ref: "9397fae65c533e8ff0828bf62511ca34082763ff" - url: "https://github.com/maplibre/flutter-maplibre-gl.git" - source: git - version: "0.18.0" + name: maplibre_gl + sha256: ea2fa443e7d5dc18db7f37a0f6f5af40642888c56b81a14441aeddea077adaea + url: "https://pub.dev" + source: hosted + version: "0.20.0" maplibre_gl_platform_interface: dependency: transitive description: - path: maplibre_gl_platform_interface - ref: "git-release-0.18.0" - resolved-ref: "9397fae65c533e8ff0828bf62511ca34082763ff" - url: "https://github.com/maplibre/flutter-maplibre-gl.git" - source: git - version: "0.18.0" + name: maplibre_gl_platform_interface + sha256: "718c3503f36936fbf35c34d6ddf8bf770474c5ba1e6cb1d8caece44efae424af" + url: "https://pub.dev" + source: hosted + version: "0.20.0" maplibre_gl_web: dependency: transitive description: - path: maplibre_gl_web - ref: "git-release-0.18.0" - resolved-ref: "9397fae65c533e8ff0828bf62511ca34082763ff" - url: "https://github.com/maplibre/flutter-maplibre-gl.git" - source: git - version: "0.18.0" + name: maplibre_gl_web + sha256: e7d71b08f24dca70e9c9cf841b096704a677e6239447d87220ec071355768149 + url: "https://pub.dev" + source: hosted + version: "0.20.0" matcher: dependency: transitive description: diff --git a/packages/app/pubspec.yaml b/packages/app/pubspec.yaml index e006bc0f..11ca7134 100644 --- a/packages/app/pubspec.yaml +++ b/packages/app/pubspec.yaml @@ -27,10 +27,7 @@ dependencies: http: 1.2.1 image_picker: 1.1.2 intl: 0.19.0 - maplibre_gl: - git: - url: https://github.com/maplibre/flutter-maplibre-gl.git - ref: 0.18.0 + maplibre_gl: 0.20.0 mime: 1.0.5 p2panda: 0.1.1 p2panda_flutter: 0.1.1 From 15e9802d4aa834630b957a774ca42e10f307fa52 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Fri, 28 Jun 2024 14:36:28 -0400 Subject: [PATCH 10/11] more data-oriented implementation approach --- packages/app/lib/locales/app_en.arb | 12 +- .../app/lib/ui/widgets/location_field.dart | 285 +++++++++++------- 2 files changed, 183 insertions(+), 114 deletions(-) diff --git a/packages/app/lib/locales/app_en.arb b/packages/app/lib/locales/app_en.arb index 2c169a21..44a9f3cb 100644 --- a/packages/app/lib/locales/app_en.arb +++ b/packages/app/lib/locales/app_en.arb @@ -96,17 +96,7 @@ } } }, - "sightingLocationFieldCoordinates": "Coordinates: Lat {latitude}, Lon {longitude}", - "@sightingLocationFieldCoordinates": { - "placeholders": { - "latitude": { - "type": "double" - }, - "longitude": { - "type": "double" - } - } - }, + "sightingLocationNoLocation": "No location set", "sightingScreenTitle": "Sighting", "sightingUnspecified": "Unknown species", "speciesCardTitle": "Species", diff --git a/packages/app/lib/ui/widgets/location_field.dart b/packages/app/lib/ui/widgets/location_field.dart index 8180a4e7..28852c08 100644 --- a/packages/app/lib/ui/widgets/location_field.dart +++ b/packages/app/lib/ui/widgets/location_field.dart @@ -1,11 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import 'package:app/ui/colors.dart'; import 'package:app/ui/widgets/action_buttons.dart'; import 'package:app/ui/widgets/editable_card.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -29,16 +29,42 @@ const Coordinates BRAZIL_CENTROID_COORDINATES = const double DEFAULT_ZOOMED_OUT_LEVEL = 1.5; const double DEFAULT_ZOOMED_IN_LEVEL = 8; +const EMPTY_SOURCE_GEOJSON_DATA = { + "type": "FeatureCollection", + // ignore: inference_failure_on_collection_literal + "features": [] +}; +const SOURCE_ID = 'points'; +const LAYER_ID = 'location'; +const BEE_IMAGE_ID = 'bee'; +const PLACEHOLDER_FEATURE_ID = 'placeholder'; +const FOCUSED_FEATURE_ID = 'focused'; + +const SHOW_FOCUSED_FILTER_EXPRESSION = [ + Expressions.equal, + [Expressions.id], + FOCUSED_FEATURE_ID, +]; +const SHOW_ALL_FILTER_EXPRESSION = [Expressions.literal, true]; +const ICON_OPACITY_EXPRESSION = [ + Expressions.caseExpression, + [ + Expressions.equal, + [Expressions.id], + PLACEHOLDER_FEATURE_ID, + ], + 0.6, + 1 +]; + class _LocationFieldState extends State { + // Represents the coordinates used for the focused symbol on the map (i.e. what's centered on the map) late Coordinates? _coordinates; bool _isEditMode = false; MapLibreMapController? _mapController; - // The marker that uses the saved coordinates of the sighting - Circle? _existingMapMarker; - @override void initState() { _coordinates = widget.coordinates; @@ -52,26 +78,25 @@ class _LocationFieldState extends State { return EditableCard( title: t.sightingLocationFieldTitle, isEditMode: _isEditMode, - onChanged: _isEditMode - ? _handleCancel - : () { - setState(() { - _isEditMode = true; - }); - }, + onChanged: _isEditMode ? _handleCancel : _handleStartEdit, child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ _renderMap(), const SizedBox( height: 20, ), - if (_coordinates != null) ...[ + if (_coordinates == null) + Text(t.sightingLocationNoLocation) + else ...[ Text(t.sightingLocationLongitude(_coordinates!.longitude), style: Theme.of(context).textTheme.bodyLarge), Text(t.sightingLocationLatitude(_coordinates!.latitude), style: Theme.of(context).textTheme.bodyLarge), ], if (_isEditMode) - ActionButtons(onCancel: _handleCancel, onAction: _handleSave) + Padding( + padding: const EdgeInsets.only(top: 10.0), + child: + ActionButtons(onCancel: _handleCancel, onAction: _handleSave)) ]), ); } @@ -90,121 +115,175 @@ class _LocationFieldState extends State { alignment: Alignment.center, height: 200, child: MapLibreMap( - initialCameraPosition: initialCameraPosition, - scrollGesturesEnabled: _isEditMode, - dragEnabled: _isEditMode, - zoomGesturesEnabled: _isEditMode, - trackCameraPosition: _isEditMode, - compassEnabled: false, - rotateGesturesEnabled: false, - onMapCreated: (controller) { - _mapController = controller; - }, - gestureRecognizers: >{ - Factory( - () => EagerGestureRecognizer()), - }, - onStyleLoadedCallback: () async { - if (_coordinates != null) { - _existingMapMarker = await _mapController!.addCircle( - createMapCircle( - latitude: _coordinates!.latitude, - longitude: _coordinates!.longitude)); - } - }, - onMapClick: (_, latLng) async { - if (!_isEditMode) return; - - final nextMarker = _mapController!.circles.where((c) { - return c != _existingMapMarker; - }).firstOrNull; - - // Add the "next" marker and update the visuals of the existing marker - if (nextMarker == null) { - await _mapController!.addCircle(createMapCircle( - latitude: latLng.latitude, longitude: latLng.longitude)); - - if (_existingMapMarker != null) { - await _mapController!.updateCircle( - _existingMapMarker!, - const CircleOptions( - circleOpacity: 0.5, - circleStrokeOpacity: 0.5, - )); - } - } else { - await _mapController! - .updateCircle(nextMarker, CircleOptions(geometry: latLng)); - } - - _mapController!.animateCamera(CameraUpdate.newLatLng(latLng)); - - setState(() { - _coordinates = - (latitude: latLng.latitude, longitude: latLng.longitude); - }); - })); + initialCameraPosition: initialCameraPosition, + scrollGesturesEnabled: _isEditMode, + dragEnabled: _isEditMode, + zoomGesturesEnabled: _isEditMode, + trackCameraPosition: _isEditMode, + compassEnabled: false, + rotateGesturesEnabled: false, + onMapCreated: (controller) { + _mapController = controller; + }, + gestureRecognizers: >{ + Factory( + () => EagerGestureRecognizer()), + }, + onStyleLoadedCallback: _handleMapStyleLoaded, + onMapClick: _handleMapPress, + )); + } + + Future _handleMapStyleLoaded() async { + // Load asset used for symbol marker + final bytes = await rootBundle.load("assets/images/meliponini.png"); + final list = bytes.buffer.asUint8List(); + _mapController!.addImage(BEE_IMAGE_ID, list); + + // Create source + await _mapController!.addGeoJsonSource( + SOURCE_ID, + _coordinates == null + ? EMPTY_SOURCE_GEOJSON_DATA + : createSourceData(focusedCoord: _coordinates!)); + + // Create layer + await _mapController!.addSymbolLayer( + SOURCE_ID, + LAYER_ID, + const SymbolLayerProperties( + iconImage: BEE_IMAGE_ID, + iconSize: 0.07, + iconAllowOverlap: true, + iconOpacity: ICON_OPACITY_EXPRESSION, + ), + enableInteraction: false, + filter: SHOW_FOCUSED_FILTER_EXPRESSION); + } + + Future _handleMapPress(_, LatLng latLng) async { + if (!_isEditMode) return; + + _mapController!.setGeoJsonSource( + SOURCE_ID, + createSourceData(focusedCoord: ( + latitude: latLng.latitude, + longitude: latLng.longitude + ), placeholderCoord: widget.coordinates)); + + _mapController!.setFilter(LAYER_ID, SHOW_ALL_FILTER_EXPRESSION); + + _mapController!.animateCamera(CameraUpdate.newLatLng(latLng)); + + setState(() { + _coordinates = (latitude: latLng.latitude, longitude: latLng.longitude); + }); + } + + Future _handleStartEdit() async { + if (widget.coordinates == null) { + await _mapController!.setGeoJsonSource(SOURCE_ID, + createSourceData(focusedCoord: BRAZIL_CENTROID_COORDINATES)); + + setState(() { + _coordinates = ( + latitude: BRAZIL_CENTROID_COORDINATES.latitude, + longitude: BRAZIL_CENTROID_COORDINATES.longitude + ); + }); + } + + setState(() { + _isEditMode = true; + }); } void _handleSave() async { - // Do nothing if coordinates have not changed at all - if (_coordinates == widget.coordinates) { + // Do nothing if coordinates they are not set or have not changed at all + if (_coordinates == null || _coordinates == widget.coordinates) { _handleCancel(); return; } setState(() { _isEditMode = false; - if (_coordinates != null) { - widget.onUpdate(_coordinates!); - } }); - _resetMap(coordinates: _coordinates!, zoomLevel: DEFAULT_ZOOMED_IN_LEVEL); + if (_coordinates != null) { + widget.onUpdate(_coordinates!); + } + + await _resetMap( + data: createSourceData(focusedCoord: _coordinates!), + cameraCoordinates: _coordinates!, + cameraZoom: DEFAULT_ZOOMED_IN_LEVEL); } void _handleCancel() async { - if (widget.coordinates == null || _coordinates == null) { + setState(() { + _isEditMode = false; + }); + + if (widget.coordinates == null) { await _resetMap( - coordinates: BRAZIL_CENTROID_COORDINATES, - zoomLevel: DEFAULT_ZOOMED_OUT_LEVEL); + data: EMPTY_SOURCE_GEOJSON_DATA, + cameraCoordinates: BRAZIL_CENTROID_COORDINATES, + cameraZoom: DEFAULT_ZOOMED_OUT_LEVEL); } else { await _resetMap( - coordinates: widget.coordinates!, zoomLevel: DEFAULT_ZOOMED_IN_LEVEL); + data: createSourceData(focusedCoord: widget.coordinates!), + cameraCoordinates: widget.coordinates!, + cameraZoom: DEFAULT_ZOOMED_IN_LEVEL, + ); } setState(() { - _isEditMode = false; _coordinates = widget.coordinates; }); } - Future _resetMap( - {required Coordinates coordinates, required double zoomLevel}) async { - final latLng = LatLng(coordinates.latitude, coordinates.longitude); - - if (_existingMapMarker != null) { - await _mapController!.updateCircle( - _existingMapMarker!, - CircleOptions( - geometry: latLng, circleOpacity: 1.0, circleStrokeOpacity: 1.0)); - } - - await _mapController!.removeCircles(_mapController!.circles.where((c) { - return c != _existingMapMarker; - })); - - _mapController! - .animateCamera(CameraUpdate.newLatLngZoom(latLng, zoomLevel)); + Future _resetMap({ + required Map data, + required Coordinates cameraCoordinates, + required double cameraZoom, + }) async { + await _mapController!.setGeoJsonSource(SOURCE_ID, data); + await _mapController!.setFilter(LAYER_ID, SHOW_FOCUSED_FILTER_EXPRESSION); + await _mapController!.animateCamera(CameraUpdate.newLatLngZoom( + LatLng(cameraCoordinates.latitude, cameraCoordinates.longitude), + cameraZoom)); } } -CircleOptions createMapCircle( - {required double latitude, required double longitude}) { - return CircleOptions( - circleRadius: 8, - circleColor: MeliColors.magnolia.toHexStringRGB(), - circleStrokeColor: MeliColors.black.toHexStringRGB(), - circleStrokeWidth: 2.0, - geometry: LatLng(latitude, longitude)); +Map createSourceData( + {required Coordinates focusedCoord, Coordinates? placeholderCoord}) { + return { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": FOCUSED_FEATURE_ID, + // ignore: inference_failure_on_collection_literal + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [focusedCoord.longitude, focusedCoord.latitude] + } + }, + if (placeholderCoord != null) + { + "type": "Feature", + "id": PLACEHOLDER_FEATURE_ID, + // ignore: inference_failure_on_collection_literal + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [ + placeholderCoord.longitude, + placeholderCoord.latitude + ] + } + } + ], + }; } From eb114738e87b8ce1dc9e45d8e0c38739a46c498e Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Fri, 28 Jun 2024 14:52:55 -0400 Subject: [PATCH 11/11] fix styling of text when no location --- packages/app/lib/ui/widgets/location_field.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app/lib/ui/widgets/location_field.dart b/packages/app/lib/ui/widgets/location_field.dart index 28852c08..307fd85e 100644 --- a/packages/app/lib/ui/widgets/location_field.dart +++ b/packages/app/lib/ui/widgets/location_field.dart @@ -2,6 +2,7 @@ import 'package:app/ui/widgets/action_buttons.dart'; import 'package:app/ui/widgets/editable_card.dart'; +import 'package:app/ui/widgets/read_only_value.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -85,7 +86,7 @@ class _LocationFieldState extends State { height: 20, ), if (_coordinates == null) - Text(t.sightingLocationNoLocation) + ReadOnlyValue(t.sightingLocationNoLocation) else ...[ Text(t.sightingLocationLongitude(_coordinates!.longitude), style: Theme.of(context).textTheme.bodyLarge),