This commit is contained in:
randogoth 2025-02-27 15:38:39 +02:00
parent ac62865cd6
commit 7409b9d4cd
3 changed files with 57 additions and 79 deletions

View file

@ -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} {"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}

View file

@ -73,11 +73,10 @@ GeoMetadataLookup({
#### **`cacheExpirationDuration` (Duration)** #### **`cacheExpirationDuration` (Duration)**
- Specifies how long cached files are kept before deletion. - Specifies how long cached files are kept before deletion.
## Testing ## Changes in Data Handling
Run unit tests with: - The library now ensures compatibility with **Point** instead of **Position** for `booleanPointInPolygon` and `_isPointNearLine`.
```sh - Web caching uses `SharedPreferences`, while file-based caching is used on other platforms.
flutter test - `lookup()` internally converts coordinates into `Point(coordinates: [longitude, latitude])`.
```
## Author ## Author
[@randogoth](https://github.com/randogoth) [@randogoth](https://github.com/randogoth)

View file

@ -2,7 +2,6 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:dart_geohash/dart_geohash.dart'; import 'package:dart_geohash/dart_geohash.dart';
import 'package:latlong2/latlong.dart';
import 'package:turf/turf.dart'; import 'package:turf/turf.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -12,7 +11,6 @@ class GeoMetadataLookup {
final String apiUrl; final String apiUrl;
final bool enableCacheExpiration; final bool enableCacheExpiration;
final Duration cacheExpirationDuration; final Duration cacheExpirationDuration;
final Distance distance = const Distance();
final http.Client httpClient; final http.Client httpClient;
Directory? cacheDir; Directory? cacheDir;
@ -36,55 +34,48 @@ class GeoMetadataLookup {
Future<List<Map<String, dynamic>>> lookup(double latitude, double longitude, {double distanceThreshold = 10.0}) async { 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 String geohash = GeoHasher().encode(latitude, longitude, precision: 4);
return kIsWeb
if (kIsWeb) { ? await _lookupWeb(geohash, latitude, longitude, distanceThreshold)
return _lookupWeb(geohash); : await _lookupFile(geohash, latitude, longitude, distanceThreshold);
} else {
return _lookupFile(geohash, latitude, longitude, distanceThreshold);
}
} }
Future<List<Map<String, dynamic>>> _lookupFile(String geohash, double latitude, double longitude, double distanceThreshold) async { Future<List<Map<String, dynamic>>> _lookupFile(String geohash, double latitude, double longitude, double distanceThreshold) async {
final File cacheFile = File('${cacheDir?.path}/$geohash.geojson.gz'); final File cacheFile = File('${cacheDir?.path}/$geohash.geojson.gz');
if (!cacheFile.existsSync() || await _isCacheExpired(geohash)) {
await _updateLastAccessTime(geohash);
final bool isExpired = await _isCacheExpired(geohash);
if (!cacheFile.existsSync() || isExpired) {
await _downloadGeoJson(geohash, cacheFile); await _downloadGeoJson(geohash, cacheFile);
} else {
await _updateLastAccessTime(geohash);
} }
final String geoJsonStr = utf8.decode(GZipCodec().decode(cacheFile.readAsBytesSync())); final String geoJsonStr = utf8.decode(GZipCodec().decode(cacheFile.readAsBytesSync()));
final Map<String, dynamic> geoJson = jsonDecode(geoJsonStr); return _findMetadata(latitude, longitude, jsonDecode(geoJsonStr), distanceThreshold);
return _findMetadata(latitude, longitude, geoJson, distanceThreshold);
} }
Future<List<Map<String, dynamic>>> _lookupWeb(String geohash) async { Future<List<Map<String, dynamic>>> _lookupWeb(String geohash, double latitude, double longitude, double distanceThreshold) async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final String? cachedGeoJson = prefs.getString('geojson_$geohash'); final String? cachedGeoJson = prefs.getString('geojson_$geohash');
if (cachedGeoJson == null || await _isCacheExpired(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) { 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); await _updateLastAccessTime(geohash);
return _findMetadata(0, 0, jsonDecode(utf8.decode(response.bodyBytes)), 10.0); return _findMetadata(latitude, longitude, jsonDecode(geoJsonStr), distanceThreshold);
} else {
throw Exception('Failed to download GeoJSON for geohash: $geohash');
} }
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<void> _downloadGeoJson(String geohash, File cacheFile) async { Future<void> _downloadGeoJson(String geohash, File cacheFile) async {
final response = await httpClient.get(Uri.parse('$apiUrl?geohash=$geohash&format=geojson')); final response = await httpClient.get(Uri.parse('$apiUrl?geohash=$geohash&format=geojson'));
if (response.statusCode == 200) { if (response.statusCode == 200) {
cacheFile.writeAsBytesSync(GZipCodec().encode(response.bodyBytes)); cacheFile.writeAsBytesSync(GZipCodec().encode(response.bodyBytes));
await _updateLastAccessTime(geohash); await _updateLastAccessTime(geohash);
} else { } else {
throw Exception('Failed to download GeoJSON for geohash: $geohash'); throw Exception('Failed to download GeoJSON for geohash: $geohash');
}
} }
}
Future<void> _updateLastAccessTime(String geohash) async { Future<void> _updateLastAccessTime(String geohash) async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
@ -95,8 +86,7 @@ class GeoMetadataLookup {
if (!enableCacheExpiration) return false; if (!enableCacheExpiration) return false;
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final int? lastAccess = prefs.getInt('cache_last_access_$geohash'); final int? lastAccess = prefs.getInt('cache_last_access_$geohash');
if (lastAccess == null) return true; return lastAccess == null || DateTime.now().difference(DateTime.fromMillisecondsSinceEpoch(lastAccess)) > cacheExpirationDuration;
return DateTime.now().difference(DateTime.fromMillisecondsSinceEpoch(lastAccess)) > cacheExpirationDuration;
} }
Future<void> _cleanExpiredCache() async { Future<void> _cleanExpiredCache() async {
@ -106,9 +96,7 @@ class GeoMetadataLookup {
final geohash = key.replaceFirst('cache_last_access_', ''); final geohash = key.replaceFirst('cache_last_access_', '');
if (await _isCacheExpired(geohash)) { if (await _isCacheExpired(geohash)) {
final File cacheFile = File('${cacheDir?.path}/$geohash.geojson.gz'); final File cacheFile = File('${cacheDir?.path}/$geohash.geojson.gz');
if (cacheFile.existsSync()) { if (cacheFile.existsSync()) cacheFile.deleteSync();
cacheFile.deleteSync();
}
await prefs.remove(key); await prefs.remove(key);
} }
} }
@ -116,50 +104,41 @@ class GeoMetadataLookup {
List<Map<String, dynamic>> _findMetadata(double latitude, double longitude, Map<String, dynamic> geoJson, double distanceThreshold) { List<Map<String, dynamic>> _findMetadata(double latitude, double longitude, Map<String, dynamic> geoJson, double distanceThreshold) {
final Position point = Position(longitude, latitude); final Position point = Position(longitude, latitude);
List<Map<String, dynamic>> results = []; final List<Map<String, dynamic>> results = [];
for (var feature in geoJson['features']) { for (var feature in geoJson['features']) {
final geometry = feature['geometry']; final geometry = feature['geometry'];
final properties = feature['properties'] ?? {}; final metadata = Map<String, dynamic>.from(feature['properties']['tags'] ?? {})..removeWhere((_, v) => v == null);
final metadata = properties['tags']?.cast<String, dynamic>() ?? {}; switch (geometry['type']) {
case 'Polygon':
metadata.removeWhere((key, value) => value == null); final polygon = Polygon(
coordinates: (geometry['coordinates'] as List)
if (geometry['type'] == 'Polygon') { .map((ring) => (ring as List)
final polygon = Polygon( .map((p) => Position(p[0].toDouble(), p[1].toDouble()))
coordinates: (geometry['coordinates'] as List) .toList())
.map((ring) => (ring as List) .toList(),
.map((p) => Position(p[0] as double, p[1] as double)) );
.toList()) if (booleanPointInPolygon(point, polygon)) results.add({"type": "polygon", ...metadata});
.toList(), break;
); case 'LineString':
if (booleanPointInPolygon(point, polygon)) { final line = LineString(
results.add({"type": "polygon", ...metadata}); coordinates: (geometry['coordinates'] as List)
} .map((p) => Position(p[0].toDouble(), p[1].toDouble()))
} .toList(),
);
else if (geometry['type'] == 'LineString') { if (_isPointNearLine(Point(coordinates: point), line, distanceThreshold)) results.add({"type": "line", ...metadata});
final line = LineString( break;
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});
}
} }
} }
return results; return results;
} }
bool _isPointNearLine(Position point, LineString line, double distanceThreshold) { bool _isPointNearLine(Point point, LineString line, double distanceThreshold) {
for (var p1 in line.coordinates) { for (final Position p in line.coordinates) {
final double dist = distance.distance( if (distance(point, Point(coordinates: p), Unit.meters) <= distanceThreshold) {
LatLng(point.lat.toDouble(), point.lng.toDouble()), return true;
LatLng(p1.lat.toDouble(), p1.lng.toDouble()), }
);
if (dist <= distanceThreshold) return true;
} }
return false; return false;
} }
} }