From 7409b9d4cd6814fb302d9beffa87cdec8e1b3b6f Mon Sep 17 00:00:00 2001 From: randogoth Date: Thu, 27 Feb 2025 15:38:39 +0200 Subject: [PATCH] refactor --- .flutter-plugins-dependencies | 2 +- README.md | 9 ++- lib/rugreefer.dart | 125 ++++++++++++++-------------------- 3 files changed, 57 insertions(+), 79 deletions(-) diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies index cbae7be..a49821a 100644 --- a/.flutter-plugins-dependencies +++ b/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"path_provider_foundation","path":"/var/home/randogoth/distrobox/randonautica/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/var/home/randogoth/distrobox/randonautica/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"android":[{"name":"path_provider_android","path":"/var/home/randogoth/distrobox/randonautica/.pub-cache/hosted/pub.dev/path_provider_android-2.2.15/","native_build":true,"dependencies":[]},{"name":"shared_preferences_android","path":"/var/home/randogoth/distrobox/randonautica/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.6/","native_build":true,"dependencies":[]}],"macos":[{"name":"path_provider_foundation","path":"/var/home/randogoth/distrobox/randonautica/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/var/home/randogoth/distrobox/randonautica/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"path_provider_linux","path":"/var/home/randogoth/distrobox/randonautica/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"shared_preferences_linux","path":"/var/home/randogoth/distrobox/randonautica/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"]}],"windows":[{"name":"path_provider_windows","path":"/var/home/randogoth/distrobox/randonautica/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]},{"name":"shared_preferences_windows","path":"/var/home/randogoth/distrobox/randonautica/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"]}],"web":[{"name":"shared_preferences_web","path":"/var/home/randogoth/distrobox/randonautica/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.3/","dependencies":[]}]},"dependencyGraph":[{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]}],"date_created":"2025-02-27 15:13:41.526644","version":"3.27.1","swift_package_manager_enabled":false} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"path_provider_foundation","path":"/var/home/randogoth/distrobox/randonautica/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/var/home/randogoth/distrobox/randonautica/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"android":[{"name":"path_provider_android","path":"/var/home/randogoth/distrobox/randonautica/.pub-cache/hosted/pub.dev/path_provider_android-2.2.15/","native_build":true,"dependencies":[]},{"name":"shared_preferences_android","path":"/var/home/randogoth/distrobox/randonautica/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.6/","native_build":true,"dependencies":[]}],"macos":[{"name":"path_provider_foundation","path":"/var/home/randogoth/distrobox/randonautica/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/var/home/randogoth/distrobox/randonautica/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"path_provider_linux","path":"/var/home/randogoth/distrobox/randonautica/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"shared_preferences_linux","path":"/var/home/randogoth/distrobox/randonautica/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"]}],"windows":[{"name":"path_provider_windows","path":"/var/home/randogoth/distrobox/randonautica/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]},{"name":"shared_preferences_windows","path":"/var/home/randogoth/distrobox/randonautica/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"]}],"web":[{"name":"shared_preferences_web","path":"/var/home/randogoth/distrobox/randonautica/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.3/","dependencies":[]}]},"dependencyGraph":[{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]}],"date_created":"2025-02-27 15:36:00.072377","version":"3.27.1","swift_package_manager_enabled":false} \ No newline at end of file diff --git a/README.md b/README.md index 6e3ba15..2b50695 100644 --- a/README.md +++ b/README.md @@ -73,11 +73,10 @@ GeoMetadataLookup({ #### **`cacheExpirationDuration` (Duration)** - Specifies how long cached files are kept before deletion. -## Testing -Run unit tests with: -```sh -flutter test -``` +## Changes in Data Handling +- The library now ensures compatibility with **Point** instead of **Position** for `booleanPointInPolygon` and `_isPointNearLine`. +- Web caching uses `SharedPreferences`, while file-based caching is used on other platforms. +- `lookup()` internally converts coordinates into `Point(coordinates: [longitude, latitude])`. ## Author [@randogoth](https://github.com/randogoth) \ No newline at end of file diff --git a/lib/rugreefer.dart b/lib/rugreefer.dart index 14d0301..9072223 100644 --- a/lib/rugreefer.dart +++ b/lib/rugreefer.dart @@ -2,7 +2,6 @@ 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'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -12,7 +11,6 @@ class GeoMetadataLookup { final String apiUrl; final bool enableCacheExpiration; final Duration cacheExpirationDuration; - final Distance distance = const Distance(); final http.Client httpClient; Directory? cacheDir; @@ -36,55 +34,48 @@ class GeoMetadataLookup { Future>> 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); - } + return kIsWeb + ? await _lookupWeb(geohash, latitude, longitude, distanceThreshold) + : await _lookupFile(geohash, latitude, longitude, distanceThreshold); } Future>> _lookupFile(String geohash, double latitude, double longitude, double distanceThreshold) async { final File cacheFile = File('${cacheDir?.path}/$geohash.geojson.gz'); - - await _updateLastAccessTime(geohash); - final bool isExpired = await _isCacheExpired(geohash); - - if (!cacheFile.existsSync() || isExpired) { + if (!cacheFile.existsSync() || await _isCacheExpired(geohash)) { await _downloadGeoJson(geohash, cacheFile); + } else { + await _updateLastAccessTime(geohash); } - final String geoJsonStr = utf8.decode(GZipCodec().decode(cacheFile.readAsBytesSync())); - final Map geoJson = jsonDecode(geoJsonStr); - return _findMetadata(latitude, longitude, geoJson, distanceThreshold); + return _findMetadata(latitude, longitude, jsonDecode(geoJsonStr), distanceThreshold); } - Future>> _lookupWeb(String geohash) async { + Future>> _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 http.get(Uri.parse('$apiUrl?geohash=$geohash&format=geojson')); + final response = await httpClient.get(Uri.parse('$apiUrl?geohash=$geohash&format=geojson')); if (response.statusCode == 200) { - await prefs.setString('geojson_$geohash', utf8.decode(response.bodyBytes)); + final geoJsonStr = utf8.decode(response.bodyBytes); + await prefs.setString('geojson_$geohash', geoJsonStr); 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(latitude, longitude, jsonDecode(geoJsonStr), distanceThreshold); } + throw Exception('Failed to download GeoJSON for geohash: $geohash'); } - return _findMetadata(0, 0, jsonDecode(cachedGeoJson), 10.0); + await _updateLastAccessTime(geohash); + return _findMetadata(latitude, longitude, jsonDecode(cachedGeoJson), distanceThreshold); } Future _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'); + 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 _updateLastAccessTime(String geohash) async { final prefs = await SharedPreferences.getInstance(); @@ -95,8 +86,7 @@ class GeoMetadataLookup { 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; + return lastAccess == null || DateTime.now().difference(DateTime.fromMillisecondsSinceEpoch(lastAccess)) > cacheExpirationDuration; } Future _cleanExpiredCache() async { @@ -106,9 +96,7 @@ class GeoMetadataLookup { 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(); - } + if (cacheFile.existsSync()) cacheFile.deleteSync(); await prefs.remove(key); } } @@ -116,50 +104,41 @@ class GeoMetadataLookup { List> _findMetadata(double latitude, double longitude, Map geoJson, double distanceThreshold) { final Position point = Position(longitude, latitude); - List> results = []; - + final List> results = []; for (var feature in geoJson['features']) { final geometry = feature['geometry']; - final properties = feature['properties'] ?? {}; - final metadata = properties['tags']?.cast() ?? {}; - - metadata.removeWhere((key, value) => value == null); - - 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(), - ); - if (booleanPointInPolygon(point, polygon)) { - results.add({"type": "polygon", ...metadata}); - } - } - - else if (geometry['type'] == 'LineString') { - 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)) { - results.add({"type": "line", ...metadata}); - } + final metadata = Map.from(feature['properties']['tags'] ?? {})..removeWhere((_, v) => v == null); + 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)) results.add({"type": "polygon", ...metadata}); + 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)) results.add({"type": "line", ...metadata}); + break; } } return results; } - bool _isPointNearLine(Position point, LineString line, double distanceThreshold) { - for (var p1 in line.coordinates) { - final double dist = distance.distance( - LatLng(point.lat.toDouble(), point.lng.toDouble()), - LatLng(p1.lat.toDouble(), p1.lng.toDouble()), - ); - if (dist <= distanceThreshold) return true; + 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; } -} \ No newline at end of file + +}