rugreefer/lib/rugreefer.dart
2025-02-28 13:53:15 +02:00

174 lines
6.9 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:dart_geohash/dart_geohash.dart';
import 'package:turf/turf.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/services.dart' show rootBundle;
class RugReefer {
final String apiUrl;
final bool enableCacheExpiration;
final Duration cacheExpirationDuration;
final http.Client httpClient;
final String categoryMap;
Directory? cacheDir;
RugReefer({
required this.apiUrl,
required this.categoryMap,
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);
final jsonMap = await loadJsonFromAssets(categoryMap);
List<Map<String, dynamic>> rawData = kIsWeb
? await _lookupWeb(geohash, latitude, longitude, distanceThreshold)
: await _lookupFile(geohash, latitude, longitude, distanceThreshold);
return _processMetadata(rawData, jsonMap, latitude, longitude, distanceThreshold);
}
Future<List<Map<String, dynamic>>> _lookupFile(String geohash, double latitude, double longitude, double distanceThreshold) async {
final File cacheFile = File('${cacheDir?.path}/$geohash.geojson.gz');
if (!cacheFile.existsSync() || await _isCacheExpired(geohash)) {
await _downloadGeoJson(geohash, cacheFile);
} else {
await _updateLastAccessTime(geohash);
}
final String geoJsonStr = utf8.decode(GZipCodec().decode(cacheFile.readAsBytesSync()));
return jsonDecode(geoJsonStr)['features'];
}
Future<List<Map<String, dynamic>>> _lookupWeb(String geohash, double latitude, double longitude, double distanceThreshold) async {
final prefs = await SharedPreferences.getInstance();
final String? cachedGeoJson = prefs.getString('geojson_$geohash');
if (cachedGeoJson == null || await _isCacheExpired(geohash)) {
final response = await httpClient.get(Uri.parse('$apiUrl?geohash=$geohash&format=geojson'));
if (response.statusCode == 200) {
final geoJsonStr = utf8.decode(response.bodyBytes);
await prefs.setString('geojson_$geohash', geoJsonStr);
await _updateLastAccessTime(geohash);
return jsonDecode(geoJsonStr)['features'];
}
throw Exception('Failed to download GeoJSON for geohash: $geohash');
}
await _updateLastAccessTime(geohash);
return jsonDecode(cachedGeoJson)['features'];
}
Future<void> _downloadGeoJson(String geohash, File cacheFile) async {
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');
return lastAccess == null || 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);
}
}
}
Future<Map<String, dynamic>> loadJsonFromAssets(String assetPath) async {
final jsonString = await rootBundle.loadString(assetPath);
return jsonDecode(jsonString);
}
List<Map<String, dynamic>> _processMetadata(List<Map<String, dynamic>> features, Map<String, dynamic> jsonMap, double latitude, double longitude, double distanceThreshold) {
final Position point = Position(longitude, latitude);
Map<String, Map<String, List<String>>> groupedObjects = {};
for (var feature in features) {
final geometry = feature['geometry'];
final metadata = Map<String, dynamic>.from(feature['properties']['tags'] ?? {})..removeWhere((_, v) => v == null);
String? mainKey;
switch (geometry['type']) {
case 'Polygon':
final polygon = Polygon(
coordinates: (geometry['coordinates'] as List)
.map((ring) => (ring as List)
.map((p) => Position(p[0].toDouble(), p[1].toDouble()))
.toList())
.toList(),
);
if (booleanPointInPolygon(point, polygon)) mainKey = 'in';
break;
case 'LineString':
final line = LineString(
coordinates: (geometry['coordinates'] as List)
.map((p) => Position(p[0].toDouble(), p[1].toDouble()))
.toList(),
);
if (_isPointNearLine(Point(coordinates: point), line, distanceThreshold)) mainKey = 'by';
break;
}
if (mainKey == null) continue;
groupedObjects.putIfAbsent(mainKey, () => {});
for (var key in metadata.keys) {
for (var category in jsonMap.keys) {
var categoryMap = jsonMap[category];
if (categoryMap is Map && categoryMap.containsKey(key)) {
var rule = categoryMap[key];
if (rule is String || (rule is List && rule.contains(metadata[key])) || rule == true) {
groupedObjects[mainKey]!.putIfAbsent(category, () => []);
groupedObjects[mainKey]![category]!.add(metadata[key]);
}
}
}
}
}
return groupedObjects.entries.map((entry) => {entry.key: entry.value}).toList();
}
bool _isPointNearLine(Point point, LineString line, double distanceThreshold) {
for (final Position p in line.coordinates) {
if (distance(point, Point(coordinates: p), Unit.meters) <= distanceThreshold) {
return true;
}
}
return false;
}
}