Skip to content

Commit

Permalink
Add OSM reverse geocoding
Browse files Browse the repository at this point in the history
  • Loading branch information
Xennis committed Sep 27, 2023
1 parent cc6e4c5 commit 4dea9fa
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 116 deletions.
1 change: 0 additions & 1 deletion green_walking/lib/pages/map/map.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart';

import 'package:green_walking/map_utils.dart';
import 'package:green_walking/services/mapbox_geocoding.dart';
import '../../services/shared_prefs.dart';
import '../../widgets/app_bar.dart';
import '../../widgets/gdpr_dialog.dart';
Expand Down
28 changes: 21 additions & 7 deletions green_walking/lib/pages/search.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
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 {
Expand All @@ -23,7 +24,7 @@ class SearchPage extends StatefulWidget {
}

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

@override
Expand Down Expand Up @@ -108,6 +109,7 @@ class _SearchPageState extends State<SearchPage> {
setState(() {
if (queryPosition != null) {
_result = mapboxReverseGeocoding(queryPosition, widget.accessToken);
//_result = osmReverseGeocoding(queryPosition);
} else {
_result = mapboxForwardGeocoding(query, widget.accessToken,
proximity: widget.proximity);
Expand All @@ -120,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 @@ -152,6 +154,7 @@ class _SearchPageState extends State<SearchPage> {
},
title: Text(truncateString(elem.text, 25) ?? ''),
subtitle: Text(subtitle),
trailing: trailingWidget(locale, elem.url),
),
);
},
Expand All @@ -162,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));
}
}
177 changes: 177 additions & 0 deletions green_walking/lib/services/geocoding.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
library mapbox_geocoding;

import 'dart:convert';
import 'dart:developer' show log;
import 'dart:io';

import 'package:green_walking/config.dart';
import 'package:http/http.dart' as http;
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart' show Position;

class GeocodingPlace {
GeocodingPlace({this.text, this.placeName, this.center, this.url});

factory GeocodingPlace.fromMapboxJson(Map<String, dynamic> raw) {
final List<dynamic>? rawCenter = raw['center'] as List<dynamic>?;
Position? center;
if (rawCenter != null && rawCenter.length == 2) {
final dynamic rawLat = rawCenter[1];
final dynamic rawLng = rawCenter[0];
center = Position(rawLng is int ? rawLng.toDouble() : rawLng as double,
rawLat is int ? rawLat.toDouble() : rawLat as double);
}
return GeocodingPlace(
text: raw['text'] as String?,
placeName: raw['place_name'] as String?,
center: center,
);
}

factory GeocodingPlace.fromOsmJson(Map<String, dynamic> raw) {
Position? center;
try {
center = Position(double.parse(raw['lon']), double.parse(raw['lat']));
} catch (e) {
log('failed to parse position: $e');
}

final int placeId = raw['place_id'] as int;
final String? type = raw['type'] as String?;
final String? name = raw['name'] as String?;
final Uri url = Uri.https('nominatim.openstreetmap.org', '/ui/details.html',
<String, String>{'place_id': placeId.toString()});

return GeocodingPlace(
text: name != null && name.isNotEmpty ? name : type,
placeName: raw['display_name'] as String?,
center: center,
url: url,
);
}

final String? text;
final String? placeName;
final Position? center;
final Uri? url;
}

class GeocodingResult {
GeocodingResult(this.features, {this.attribution});

factory GeocodingResult.fromMapboxJson(Map<String, dynamic> raw) {
final List<dynamic> features =
raw['features'] as List<dynamic>? ?? <dynamic>[];
return GeocodingResult(
features
.map((dynamic e) => e as Map<String, dynamic>)
.map((Map<String, dynamic> e) => GeocodingPlace.fromMapboxJson(e))
.where((element) =>
element.text != null &&
element.placeName != null &&
element.center != null)
.toList(),
attribution: raw['attribution'] as String?);
}

factory GeocodingResult.fromOsmJson(Map<String, dynamic> raw) {
final List<GeocodingPlace> features = [GeocodingPlace.fromOsmJson(raw)];

return GeocodingResult(
features
.where((element) =>
element.text != null &&
element.placeName != null &&
element.center != null)
.toList(),
attribution: raw['licence'] as String?);
}

final List<GeocodingPlace> features;
final String? attribution;
}

class GeocodingServiceException implements Exception {
GeocodingServiceException(this.cause);
String cause;
}

Future<GeocodingResult> _mapboxGeocoding(
String query, String token, Map<String, String> params) async {
final Uri url = Uri.https(
'api.mapbox.com', '/geocoding/v5/mapbox.places/$query.json', params);
try {
final http.Response response = await http.get(url);
if (response.statusCode == 200) {
return GeocodingResult.fromMapboxJson(
json.decode(response.body) as Map<String, dynamic>);
} else {
throw GeocodingServiceException(
'Invalid ${response.statusCode} response from API');
}
} on SocketException catch (e) {
log('mapbox geocoding socket failed: $e');
throw GeocodingServiceException('No internet connection');
} catch (e) {
log('mapbox geocoding failed: $e');
throw GeocodingServiceException('Internal error');
}
}

// Note For debugging https://docs.mapbox.com/playground/geocoding/ is helpful
Future<GeocodingResult> mapboxForwardGeocoding(String query, String token,
{Position? proximity, int limit = 5}) async {
final Map<String, String> params = <String, String>{
'access_token': token,
'limit': limit.toString(),
};
if (proximity != null) {
params['proximity'] = _mapboxPositionToString(proximity);
}
return _mapboxGeocoding(query, token, params);
}

// Note For debugging https://docs.mapbox.com/playground/geocoding/ is helpful
// Consider improving it: https://nominatim.openstreetmap.org/ui/reverse.html
Future<GeocodingResult> mapboxReverseGeocoding(Position query, String token,
{int limit = 1}) async {
final Map<String, String> params = <String, String>{
'access_token': token,
// Not included: country,region,postcode,district,locality,neighborhood,place,poi
'types': 'address',
// If multiple types are set the limit must not be set.
'limit': limit.toString(),
};
return _mapboxGeocoding(_mapboxPositionToString(query), token, params);
}

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

Future<GeocodingResult> osmReverseGeocoding(Position position,
{int zoom = 18}) async {
final Uri url =
Uri.https('nominatim.openstreetmap.org', 'reverse.php', <String, String>{
'lat': position.lat.toStringAsFixed(6),
'lon': position.lng.toStringAsFixed(6),
'zoom': zoom.toString(),
'format': 'jsonv2'
});
try {
final http.Response response = await http.get(url,
headers: <String, String>{'User-Agent': 'Android app $androidAppID'});
if (response.statusCode == 200) {
return GeocodingResult.fromOsmJson(
json.decode(response.body) as Map<String, dynamic>);
} else {
throw GeocodingServiceException(
'Invalid ${response.statusCode} response from API');
}
} on SocketException catch (e) {
log('osm geocoding socket failed: $e');
throw GeocodingServiceException('No internet connection');
} catch (e) {
log('osm geocoding failed: $e');
throw GeocodingServiceException('Internal error');
}
}
108 changes: 0 additions & 108 deletions green_walking/lib/services/mapbox_geocoding.dart

This file was deleted.

0 comments on commit 4dea9fa

Please sign in to comment.