Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add reverse geocoding #201

Merged
merged 2 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions green_walking/lib/map_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ extension PuckPosition on StyleManager {
}
}
}

Position positionForCoordinate(Map<String?, Object?> raw) {
final List<Object?> coordinates = raw['coordinates'] as List<Object?>;
return Position(coordinates[0] as num, coordinates[1] as num);
}
71 changes: 49 additions & 22 deletions green_walking/lib/pages/map/map.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import 'dart:developer' show log;

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:green_walking/map_utils.dart';
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart';

import 'package:green_walking/map_utils.dart';
import '../../services/shared_prefs.dart';
import '../../widgets/app_bar.dart';
import '../../widgets/gdpr_dialog.dart';
Expand Down Expand Up @@ -81,7 +81,7 @@ class _MapPageState extends State<MapPage> {
Flexible(
child: Stack(
children: <Widget>[
map(context, data),
_mapWidget(context, data),
/*Attribution(
satelliteLayer:
_mapboxStyle == MabboxTileset.satellite),*/
Expand Down Expand Up @@ -131,7 +131,7 @@ class _MapPageState extends State<MapPage> {
);
}

Widget map(BuildContext context, MapConfig config) {
Widget _mapWidget(BuildContext context, MapConfig config) {
return MapWidget(
key: const ValueKey('mapWidget'),
resourceOptions: ResourceOptions(accessToken: config.accessToken),
Expand All @@ -154,6 +154,8 @@ class _MapPageState extends State<MapPage> {
zoom: 11.0),
styleUri: CustomMapboxStyles.outdoor,
onMapIdleListener: _onCameraIdle,
onLongTapListener: (ScreenCoordinate coordinate) =>
_onLongTapListener(context, config.accessToken, coordinate),
onScrollListener: (ScreenCoordinate coordinate) {
if (_userlocationTracking.value != UserLocationTracking.no) {
// Turn off tracking because user scrolled to another location.
Expand All @@ -163,12 +165,33 @@ class _MapPageState extends State<MapPage> {
);
}

Future<void> _onLongTapListener(BuildContext context, String accesstoken,
ScreenCoordinate coordinate) async {
try {
// https://github.com/mapbox/mapbox-maps-flutter/issues/81 It actual returns the position
// and not the coordinates.
final Position tapPosition = Position(coordinate.y, coordinate.x);

// See https://dart.dev/tools/linter-rules/use_build_context_synchronously
if (context.mounted) {
final Position? moveToLoc = await Navigator.push(
context,
NoTransitionPageRoute<Position>(
builder: (BuildContext context) => SearchPage(
reversePosition: tapPosition, accessToken: accesstoken)),
);
return _displaySearchResult(moveToLoc);
}
} catch (e) {
log('failed to get tap position: $e');
return;
}
}

Future<Position?> _getCameraPosition() async {
try {
final CameraState mapCameraState = await _mapboxMap.getCameraState();
final List<Object?> coordinates =
mapCameraState.center['coordinates'] as List<Object?>;
return Position(coordinates[0] as num, coordinates[1] as num);
return positionForCoordinate(mapCameraState.center);
} catch (e) {
log('failed to get camera position: $e');
return null;
Expand All @@ -186,25 +209,29 @@ class _MapPageState extends State<MapPage> {
context,
NoTransitionPageRoute<Position>(
builder: (BuildContext context) => SearchPage(
mapPosition: cameraPosition, accessToken: accessToken)),
proximity: cameraPosition, accessToken: accessToken)),
);
if (moveToLoc == null) {
return;
}
// If we keep the tracking on the map would move back to the user location.
_userlocationTracking.value = UserLocationTracking.no;
_setCameraPosition(moveToLoc, 0, 0);
return _displaySearchResult(moveToLoc);
}
}

// Draw circle
await _circleAnnotationManager?.deleteAll();
_circleAnnotationManager?.create(CircleAnnotationOptions(
geometry: Point(coordinates: moveToLoc).toJson(),
circleRadius: 12,
circleColor: const Color.fromRGBO(255, 192, 203, 1).value,
circleOpacity: 0.6,
circleStrokeWidth: 2,
circleStrokeColor: const Color.fromRGBO(255, 192, 203, 1).value));
Future<void> _displaySearchResult(Position? position) async {
if (position == null) {
return;
}
// If we keep the tracking on the map would move back to the user location.
_userlocationTracking.value = UserLocationTracking.no;
_setCameraPosition(position, 0, 0);

// Draw circle
await _circleAnnotationManager?.deleteAll();
_circleAnnotationManager?.create(CircleAnnotationOptions(
geometry: Point(coordinates: position).toJson(),
circleRadius: 12,
circleColor: const Color.fromRGBO(255, 192, 203, 1).value,
circleOpacity: 0.6,
circleStrokeWidth: 2,
circleStrokeColor: const Color.fromRGBO(255, 192, 203, 1).value));
}

void _onLayerToggle() async {
Expand Down
83 changes: 72 additions & 11 deletions green_walking/lib/pages/search.dart
Original file line number Diff line number Diff line change
@@ -1,28 +1,69 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart' show Position;
import 'package:url_launcher/url_launcher.dart';

import '../core.dart';
import '../services/mapbox_geocoding.dart';
import '../services/geocoding.dart';
import '../widgets/app_bar.dart';

class SearchPage extends StatefulWidget {
const SearchPage({Key? key, this.mapPosition, required this.accessToken})
const SearchPage(
{Key? key,
this.reversePosition,
this.proximity,
required this.accessToken})
: super(key: key);

final String accessToken;
final Position? mapPosition;
final Position? reversePosition;
final Position? proximity;

@override
State<SearchPage> createState() => _SearchPageState();
}

class _SearchPageState extends State<SearchPage> {
Future<MapboxGeocodingResult>? _result;
Future<GeocodingResult>? _result;
late TextEditingController _queryFieldController;

@override
void initState() {
super.initState();
_queryFieldController = TextEditingController();
final initialQuery = widget.reversePosition;
if (initialQuery != null) {
_queryFieldController.text = _positionToString(initialQuery);
}
}

@override
void dispose() {
_queryFieldController.dispose();
super.dispose();
}

static String _positionToString(Position position, {int fractionDigits = 6}) {
return '${position.lat.toStringAsFixed(fractionDigits)},${position.lng.toStringAsFixed(fractionDigits)}';
}

static Position? _stringToPosition(String position) {
final List<String> parts = position.split(',');
if (parts.length != 2) {
return null;
}
try {
// Yes, [1] comes first. We are showing the lat in the beginning.
return Position(double.parse(parts[1]), double.parse(parts[0]));
} catch (e) {
return null;
}
}

@override
Widget build(BuildContext context) {
final AppLocalizations locale = AppLocalizations.of(context)!;

return Scaffold(
// If the search in the search bar is clicked the keyboard appears. The keyboard
// should be over the map and by that avoid resizing of the whole app / map.
Expand All @@ -39,6 +80,7 @@ class _SearchPageState extends State<SearchPage> {
.backButtonTooltip),
onPressed: () => Navigator.pop(context)),
title: TextField(
controller: _queryFieldController,
autofocus: true,
cursorColor: Colors.black,
keyboardType: TextInputType.text,
Expand All @@ -62,9 +104,16 @@ class _SearchPageState extends State<SearchPage> {
}

Future<void> _onSearchSubmitted(String query) async {
_queryFieldController.text = query;
final Position? queryPosition = _stringToPosition(query);
setState(() {
_result =
mapboxGeocodingGet(query, widget.accessToken, widget.mapPosition);
if (queryPosition != null) {
_result = mapboxReverseGeocoding(queryPosition, widget.accessToken);
//_result = osmReverseGeocoding(queryPosition);
} else {
_result = mapboxForwardGeocoding(query, widget.accessToken,
proximity: widget.proximity);
}
});
}

Expand All @@ -73,19 +122,19 @@ class _SearchPageState extends State<SearchPage> {
return Container();
}
final AppLocalizations locale = AppLocalizations.of(context)!;
return FutureBuilder<MapboxGeocodingResult>(
return FutureBuilder<GeocodingResult>(
future: _result,
builder: (BuildContext context,
AsyncSnapshot<MapboxGeocodingResult> snapshot) {
final MapboxGeocodingResult? data = snapshot.data;
builder:
(BuildContext context, AsyncSnapshot<GeocodingResult> snapshot) {
final GeocodingResult? data = snapshot.data;
if (snapshot.hasData && data != null) {
if (data.features.isEmpty) {
return Text(locale.searchNoResultsText);
}
return ListView.builder(
itemCount: data.features.length,
itemBuilder: (BuildContext context, int index) {
final MaboxGeocodingPlace elem = data.features[index];
final GeocodingPlace elem = data.features[index];
final String subtitle = truncateString(
elem.placeName
?.replaceFirst('${elem.text ?? ''}, ', ''),
Expand All @@ -105,6 +154,7 @@ class _SearchPageState extends State<SearchPage> {
},
title: Text(truncateString(elem.text, 25) ?? ''),
subtitle: Text(subtitle),
trailing: trailingWidget(locale, elem.url),
),
);
},
Expand All @@ -115,4 +165,15 @@ class _SearchPageState extends State<SearchPage> {
return const Center(child: CircularProgressIndicator());
});
}

Widget? trailingWidget(AppLocalizations locale, Uri? url) {
if (url == null) {
return null;
}
return IconButton(
splashColor: Colors.grey,
icon: Icon(Icons.open_in_new,
semanticLabel: locale.openInBrowserSemanticLabel),
onPressed: () => launchUrl(url));
}
}
Loading