import 'dart:async'; import 'dart:isolate'; import 'package:aves/geo/topojson.dart'; import 'package:collection/collection.dart'; import 'package:country_code/country_code.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:latlong2/latlong.dart'; final CountryTopology countryTopology = CountryTopology._private(); class CountryTopology { static const topoJsonAsset = 'assets/countries-50m.json'; CountryTopology._private(); Topology? _topology; Future getTopology() => _topology != null ? SynchronousFuture(_topology) : rootBundle.loadString(topoJsonAsset).then(TopoJson().parse); // returns the country containing given coordinates Future countryCode(LatLng position) async { return _countryOfNumeric(await numericCode(position)); } // returns the ISO 3166-1 numeric code of the country containing given coordinates Future numericCode(LatLng position) async { final topology = await getTopology(); if (topology == null) return null; final countries = (topology.objects['countries'] as GeometryCollection).geometries; return _getNumeric(topology, countries, position); } // returns a map of the given positions by country Future>> countryCodeMap(Set positions) async { final numericMap = await numericCodeMap(positions); if (numericMap == null) return {}; return Map.fromEntries(numericMap.entries.map((kv) { final code = _countryOfNumeric(kv.key); return code != null ? MapEntry(code, kv.value) : null; }).nonNulls); } // returns a map of the given positions by the ISO 3166-1 numeric code of the country containing them Future>?> numericCodeMap(Set positions) async { final topology = await getTopology(); if (topology == null) return null; try { return Isolate.run>>(() { final countries = (topology.objects['countries'] as GeometryCollection).geometries; final byCode = >{}; for (final position in positions) { final code = _getNumeric(topology, countries, position); if (code != null) { byCode[code] = (byCode[code] ?? {})..add(position); } } return byCode; }); } catch (error, stack) { debugPrint('failed to get country codes with error=$error\n$stack'); return null; } } static int? _getNumeric(Topology topology, List mruCountries, LatLng position) { final point = [position.longitude, position.latitude]; final hit = mruCountries.firstWhereOrNull((country) => country.containsPoint(topology, point)); if (hit == null) return null; // promote hit countries, assuming given positions are likely to come from the same countries if (mruCountries.first != hit) { mruCountries.remove(hit); mruCountries.insert(0, hit); } final idString = (hit.id as String?); final code = idString == null ? null : int.tryParse(idString); return code; } static CountryCode? _countryOfNumeric(int? numeric) { if (numeric == null) return null; try { return CountryCode.ofNumeric(numeric); } catch (error) { debugPrint('failed to find country for numeric=$numeric with error=$error'); } return null; } }