rugreefer/lib/rugreefer.dart

165 lines
6 KiB
Dart
Raw Normal View History

2025-02-27 13:43:31 +02:00
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:dart_geohash/dart_geohash.dart';
import 'package:latlong2/latlong.dart';
import 'package:turf/turf.dart';
2025-02-27 15:24:16 +02:00
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
2025-02-27 13:43:31 +02:00
class GeoMetadataLookup {
final String apiUrl;
2025-02-27 15:24:16 +02:00
final bool enableCacheExpiration;
final Duration cacheExpirationDuration;
2025-02-27 13:43:31 +02:00
final Distance distance = const Distance();
2025-02-27 15:24:16 +02:00
final http.Client httpClient;
Directory? cacheDir;
2025-02-27 13:43:31 +02:00
GeoMetadataLookup({
required this.apiUrl,
2025-02-27 15:24:16 +02:00
this.enableCacheExpiration = false,
this.cacheExpirationDuration = const Duration(days: 14),
http.Client? client,
}) : httpClient = client ?? http.Client() {
_initCacheDir();
}
Future<void> _initCacheDir() async {
if (!kIsWeb) {
cacheDir = await getApplicationDocumentsDirectory();
if (enableCacheExpiration) {
await _cleanExpiredCache();
}
}
}
Future<List<Map<String, dynamic>>> lookup(double latitude, double longitude, {double distanceThreshold = 10.0}) async {
final String geohash = GeoHasher().encode(latitude, longitude, precision: 4);
if (kIsWeb) {
return _lookupWeb(geohash);
} else {
return _lookupFile(geohash, latitude, longitude, distanceThreshold);
2025-02-27 13:43:31 +02:00
}
}
2025-02-27 15:24:16 +02:00
Future<List<Map<String, dynamic>>> _lookupFile(String geohash, double latitude, double longitude, double distanceThreshold) async {
final File cacheFile = File('${cacheDir?.path}/$geohash.geojson.gz');
2025-02-27 13:43:31 +02:00
2025-02-27 15:24:16 +02:00
await _updateLastAccessTime(geohash);
final bool isExpired = await _isCacheExpired(geohash);
if (!cacheFile.existsSync() || isExpired) {
2025-02-27 13:43:31 +02:00
await _downloadGeoJson(geohash, cacheFile);
}
final String geoJsonStr = utf8.decode(GZipCodec().decode(cacheFile.readAsBytesSync()));
final Map<String, dynamic> geoJson = jsonDecode(geoJsonStr);
2025-02-27 15:24:16 +02:00
return _findMetadata(latitude, longitude, geoJson, distanceThreshold);
}
Future<List<Map<String, dynamic>>> _lookupWeb(String geohash) async {
final prefs = await SharedPreferences.getInstance();
final String? cachedGeoJson = prefs.getString('geojson_$geohash');
if (cachedGeoJson == null || await _isCacheExpired(geohash)) {
final response = await http.get(Uri.parse('$apiUrl?geohash=$geohash&format=geojson'));
if (response.statusCode == 200) {
await prefs.setString('geojson_$geohash', utf8.decode(response.bodyBytes));
await _updateLastAccessTime(geohash);
return _findMetadata(0, 0, jsonDecode(utf8.decode(response.bodyBytes)), 10.0);
} else {
throw Exception('Failed to download GeoJSON for geohash: $geohash');
}
}
return _findMetadata(0, 0, jsonDecode(cachedGeoJson), 10.0);
2025-02-27 13:43:31 +02:00
}
Future<void> _downloadGeoJson(String geohash, File cacheFile) async {
2025-02-27 15:24:16 +02:00
final response = await httpClient.get(Uri.parse('$apiUrl?geohash=$geohash&format=geojson'));
if (response.statusCode == 200) {
cacheFile.writeAsBytesSync(GZipCodec().encode(response.bodyBytes));
await _updateLastAccessTime(geohash);
} else {
throw Exception('Failed to download GeoJSON for geohash: $geohash');
}
}
Future<void> _updateLastAccessTime(String geohash) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('cache_last_access_$geohash', DateTime.now().millisecondsSinceEpoch);
}
Future<bool> _isCacheExpired(String geohash) async {
if (!enableCacheExpiration) return false;
final prefs = await SharedPreferences.getInstance();
final int? lastAccess = prefs.getInt('cache_last_access_$geohash');
if (lastAccess == null) return true;
return DateTime.now().difference(DateTime.fromMillisecondsSinceEpoch(lastAccess)) > cacheExpirationDuration;
}
Future<void> _cleanExpiredCache() async {
final prefs = await SharedPreferences.getInstance();
final keys = prefs.getKeys().where((key) => key.startsWith('cache_last_access_')).toList();
for (var key in keys) {
final geohash = key.replaceFirst('cache_last_access_', '');
if (await _isCacheExpired(geohash)) {
final File cacheFile = File('${cacheDir?.path}/$geohash.geojson.gz');
if (cacheFile.existsSync()) {
cacheFile.deleteSync();
}
await prefs.remove(key);
}
2025-02-27 13:43:31 +02:00
}
}
2025-02-27 15:24:16 +02:00
List<Map<String, dynamic>> _findMetadata(double latitude, double longitude, Map<String, dynamic> geoJson, double distanceThreshold) {
2025-02-27 14:38:03 +02:00
final Position point = Position(longitude, latitude);
2025-02-27 13:43:31 +02:00
List<Map<String, dynamic>> results = [];
for (var feature in geoJson['features']) {
final geometry = feature['geometry'];
final properties = feature['properties'] ?? {};
2025-02-27 15:24:16 +02:00
final metadata = properties['tags']?.cast<String, dynamic>() ?? {};
metadata.removeWhere((key, value) => value == null);
2025-02-27 13:43:31 +02:00
if (geometry['type'] == 'Polygon') {
final polygon = Polygon(
coordinates: (geometry['coordinates'] as List)
.map((ring) => (ring as List)
.map((p) => Position(p[0] as double, p[1] as double))
.toList())
.toList(),
);
2025-02-27 14:38:03 +02:00
if (booleanPointInPolygon(point, polygon)) {
2025-02-27 15:24:16 +02:00
results.add({"type": "polygon", ...metadata});
2025-02-27 14:38:03 +02:00
}
2025-02-27 13:43:31 +02:00
}
else if (geometry['type'] == 'LineString') {
2025-02-27 14:38:03 +02:00
final line = LineString(
coordinates: (geometry['coordinates'] as List)
.map((p) => Position(p[0] as double, p[1] as double))
.toList(),
);
if (_isPointNearLine(point, line, distanceThreshold)) {
2025-02-27 15:24:16 +02:00
results.add({"type": "line", ...metadata});
2025-02-27 13:43:31 +02:00
}
}
}
return results;
}
2025-02-27 14:38:03 +02:00
bool _isPointNearLine(Position point, LineString line, double distanceThreshold) {
2025-02-27 15:24:16 +02:00
for (var p1 in line.coordinates) {
2025-02-27 13:43:31 +02:00
final double dist = distance.distance(
2025-02-27 14:38:03 +02:00
LatLng(point.lat.toDouble(), point.lng.toDouble()),
2025-02-27 13:43:31 +02:00
LatLng(p1.lat.toDouble(), p1.lng.toDouble()),
);
if (dist <= distanceThreshold) return true;
}
return false;
}
}