diff --git a/lib/app/geocode/nominatim.dart b/lib/app/geocode/nominatim.dart index d244f9d4..a1ba08d1 100644 --- a/lib/app/geocode/nominatim.dart +++ b/lib/app/geocode/nominatim.dart @@ -239,6 +239,48 @@ class Nominatim { final data = json.decode(response.body) as List; return data.map((p) => Place.fromJson(p as Map)).toList(); } + + static Future details({ + required String osmType, + required int osmId, + String? className, + bool addressDetails = false, + bool keywords = false, + bool linkedPlaces = false, + bool hierarchy = false, + bool group_hierarchy = false, + bool polygon_geojson = false, + String? language, + }) async { + final baseServer = Uri.parse(Config.addressSearchUrl); + assert(baseServer.scheme == 'https', 'It\'s required to have the address search on https'); + + assert( + ['N', 'W', 'R', null].contains(osmType), + 'osmType needs to be one of N, W, R', + ); + + final uri = Uri.https( + baseServer.host, + '${baseServer.path}/details', + { + 'format': 'json', + 'osmtype': osmType, + 'osmid': osmId.toString(), + if (className != null) 'class': className, + if (addressDetails) 'addressdetails': '1', + if (keywords) 'keywords': '1', + if (linkedPlaces) 'linkedplaces': '1', + if (hierarchy) 'hierarchy': '1', + if (group_hierarchy) 'group_hierarchy': '1', + if (polygon_geojson) 'polygon_geojson': '1', + if (language != null) 'accept-language': language, + }, + ); + final response = await http.get(uri); + final data = json.decode(response.body) as Map; + return AddressDetail.fromJson(data as Map); + } } /// A place in the nominatim system @@ -337,6 +379,34 @@ class Place { final Map? nameDetails; } +/// A place in the nominatim system +class AddressDetail { + // ignore: public_member_api_docs + AddressDetail({ + required this.osmType, + required this.osmId, + required this.addressTags, + }); + + // ignore: public_member_api_docs + factory AddressDetail.fromJson(Map json) => AddressDetail( + osmType: json['osm_type'] != null ? json['osm_type'] as String : null, + osmId: json['osm_id'] != null ? json['osm_id'] as int : null, + addressTags: json['addresstags'] != null ? json['addresstags'] as Map : null, + ); + + /// Reference to the OSM object + final String? osmType; + + /// Reference to the OSM object + final int? osmId; + + /// Map of address tags + /// Only with [Nominatim.searchByName(addressDetails: true)] + /// See https://nominatim.org/release-docs/latest/api/Output/#addressdetails + final Map? addressTags; +} + /// View box for searching class ViewBox { // ignore: public_member_api_docs diff --git a/lib/app/services/converters/place_parser.dart b/lib/app/services/converters/place_parser.dart index 9f1cfd50..6f2554fb 100644 --- a/lib/app/services/converters/place_parser.dart +++ b/lib/app/services/converters/place_parser.dart @@ -4,7 +4,7 @@ extension PlaceParser on Place { String getAddress() { var road = getRoad(); var housenumber = getHouseNumber(); - var city = getCity(); + var city = getCityOrVillage(); var zipCode = getZipCode(); var state = getState(); var municipality = getMunicipality(); @@ -46,8 +46,13 @@ extension PlaceParser on Place { ''; } + String getCityOrVillage() { + var city = getCity(); + return city.isEmpty ? (address?['village']?.toString() ?? '') : city; + } + String getCity() { - return address?['city']?.toString() ?? address?['town']?.toString() ?? address?['village']?.toString() ?? ''; + return address?['city']?.toString() ?? address?['town']?.toString() ?? ''; } String getZipCode() { @@ -74,3 +79,9 @@ extension PlaceParser on Place { return address?['county']?.toString() ?? ''; } } + +extension AdressDetailParser on AddressDetail { + String? getCity() { + return addressTags?['city']?.toString(); + } +} diff --git a/lib/app/services/nominatim_service.dart b/lib/app/services/nominatim_service.dart index 35b9ba8d..a91e0b91 100644 --- a/lib/app/services/nominatim_service.dart +++ b/lib/app/services/nominatim_service.dart @@ -17,7 +17,20 @@ class NominatimService { lat: location.latitude, lon: location.longitude, ); - final address = AddressModel.fromPlace(place); + String? cityOverride; + if (place.getCity().isEmpty) { + var osmType = switch (place.osmType) { + 'way' => 'W', + 'node' => 'N', + 'relation' => 'R', + String() => throw UnimplementedError(), + null => throw UnimplementedError(), + }; + final addressDetail = await Nominatim.details(osmType: osmType, osmId: place.osmId!); + cityOverride = addressDetail.getCity(); + } + + final address = AddressModel.fromPlace(place, cityOverride: cityOverride); return address; } catch (e) { _logger.e( @@ -105,9 +118,9 @@ class AddressModel { * For City we take in descending order city, town, or village, whatever is first. * Hopefully, this will solve most issues with that kind of address composition. */ - AddressModel.fromPlace(Place place) + AddressModel.fromPlace(Place place, {String? cityOverride}) : street = place.getRoad(), houseNumber = place.getHouseNumber(), zipCode = place.getZipCode(), - city = place.getCity(); + city = cityOverride ?? place.getCityOrVillage(); } diff --git a/lib/features/campaigns/screens/map_consumer.dart b/lib/features/campaigns/screens/map_consumer.dart index f2baacd6..68fccbc3 100644 --- a/lib/features/campaigns/screens/map_consumer.dart +++ b/lib/features/campaigns/screens/map_consumer.dart @@ -68,7 +68,7 @@ abstract class MapConsumer extends State with Focus AppRoute( builder: (context) { return FutureBuilder( - future: locationAddress.timeout(const Duration(milliseconds: 800), onTimeout: () => AddressModel()), + future: locationAddress.timeout(const Duration(milliseconds: 1300), onTimeout: () => AddressModel()), builder: (context, AsyncSnapshot snapshot) { if (!snapshot.hasData && !snapshot.hasError) { return Container( diff --git a/lib/main.dart b/lib/main.dart index c27a5c0b..f961a24a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -40,13 +40,13 @@ void main() async { } registerSecureStorage(); - GetIt.I.registerSingleton(GrueneApiCampaignsStatisticsService()); GetIt.I.registerSingleton(AppSettings()); GetIt.I.registerFactory(MfaFactory.create); GetIt.I.registerSingleton(IpService()); // Warning: The gruene api singleton depends on the auth repository which depends on the authenticator singleton // Therefore this should be last GetIt.I.registerSingleton(await createGrueneApiClient()); + GetIt.I.registerSingleton(GrueneApiCampaignsStatisticsService()); GetIt.I.registerFactory(() => NominatimService(countryCode: t.campaigns.search.country_code)); runApp(TranslationProvider(child: const MyApp()));