diff --git a/assets/map.json b/assets/map.json new file mode 100644 index 0000000..2f766e1 --- /dev/null +++ b/assets/map.json @@ -0,0 +1,287 @@ +{ + "abandoned": { + "historic": "ruins", + "landuse": "abandoned", + "railway": [ + "abandoned", + "disused" + ] + }, + "airfield": { + "aeroway": true + }, + "archeologic": { + "historic": "archaeological_site" + }, + "beach": { + "natural": [ + "beach", + "coastline", + "shingle", + "shoal" + ] + }, + "border": { + "boundary": [ + "border_zone", + "disputed" + ] + }, + "bush": { + "natural": [ + "scrub", + "shrubbery" + ] + }, + "cemetery": { + "landuse": [ + "cemetery", + "grave_yard", + "graveyard" + ] + }, + "commercial": { + "landuse": [ + "commercial", + "retail" + ] + }, + "construction": { + "landuse": [ + "brownfield", + "construction" + ], + "railway": "construction" + }, + "farmland": { + "landuse": [ + "agricultural", + "agriculture", + "animal_keeping", + "farmland", + "farmyard", + "greenhouse_horticulture", + "orchard", + "paddy", + "plant_nursery", + "vineyard" + ], + "place": [ + "farm", + "plot" + ] + }, + "forest": { + "boundary": "forest", + "landuse": [ + "forest", + "forestry", + "logging" + ], + "natural": [ + "tree_row", + "wood" + ], + "place": "woodland" + }, + "fort": { + "historic": [ + "castle_crush", + "castle_wall", + "castle", + "fort" + ] + }, + "grassland": { + "landuse": [ + "clearing", + "grass", + "grassland", + "green", + "greenfield", + "meadow", + "pasture" + ], + "natural": [ + "grassland", + "heath", + "tundra" + ] + }, + "greenery": { + "landuse": "greenery" + }, + "hazard": { + "boundary": "hazard", + "geological": "sinkhole" + }, + "history": { + "historic": [ + "aqueduct", + "battlefield", + "citywalls", + "yes" + ] + }, + "industrial": { + "landuse": "industrial", + "power": "plant" + }, + "island": { + "place": [ + "island", + "islet" + ] + }, + "landfill": { + "landuse": "landfill" + }, + "lithic": { + "geological": [ + "columnar_jointing", + "dyke", + "glacial_erratic", + "hoodoo", + "inselberg", + "limestone_pavement", + "monocline", + "moraine", + "outcrop", + "pingo", + "tor" + ], + "landuse": "quarry", + "natural": [ + "bare_rock", + "scree", + "stone" + ] + }, + "meteor": { + "geological": "meteor_crater" + }, + "military": { + "landuse": "military", + "military": [ + "airfield", + "barracks", + "base", + "bunker", + "danger_area", + "nuclear_explosion_site", + "range", + "training_area" + ] + }, + "park": { + "boundary": "national_park", + "landuse": [ + "park", + "village_green" + ], + "leisure": [ + "dog_park", + "garden", + "park" + ] + }, + "protected": { + "boundary": [ + "aboriginal_lands", + "protected_area" + ], + "landuse": "conservation", + "leisure": "nature_reserve" + }, + "railway": { + "landuse": "railway", + "railway": "rail" + }, + "recreation": { + "landuse": [ + "playground", + "recreation_ground", + "recreation" + ], + "leisure": [ + "miniature_gold", + "playground", + "water_park" + ] + }, + "residential": { + "landuse": [ + "allotments", + "residential" + ] + }, + "spiritual": { + "historic": [ + "church", + "mosque", + "temple" + ], + "landuse": [ + "churchyard", + "religious" + ] + }, + "sports": { + "landuse": "winter_sports", + "sport": [ + "bmx", + "golf", + "paintball", + "skateboard", + "skiing", + "speedway", + "tennis" + ] + }, + "tourism": { + "tourism": [ + "attraction", + "yes" + ] + }, + "volcano": { + "geological": [ + "volcanic_caldera_rim", + "volcanic_lava_field", + "volcanic_vent" + ], + "natural": "volcano" + }, + "wasteland": { + "landuse": "wasteland", + "natural": [ + "desert", + "mud", + "sand" + ] + }, + "water": { + "landuse": [ + "basin", + "reservoir", + "salt_pond" + ], + "natural": [ + "strait", + "water", + "wetland" + ], + "place": [ + "ocean", + "sea" + ], + "water": true, + "waterway": [ + "river", + "stream", + "tidal_channel", + "canal", + "fairway" + ] + } +} \ No newline at end of file diff --git a/lib/rugreefer.dart b/lib/rugreefer.dart index 6fa64ac..8e383b4 100644 --- a/lib/rugreefer.dart +++ b/lib/rugreefer.dart @@ -6,16 +6,19 @@ 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, @@ -34,9 +37,13 @@ class RugReefer { Future>> lookup(double latitude, double longitude, {double distanceThreshold = 10.0}) async { final String geohash = GeoHasher().encode(latitude, longitude, precision: 4); - return kIsWeb + final jsonMap = await loadJsonFromAssets(categoryMap); + + List> rawData = kIsWeb ? await _lookupWeb(geohash, latitude, longitude, distanceThreshold) : await _lookupFile(geohash, latitude, longitude, distanceThreshold); + + return _processMetadata(rawData, jsonMap, latitude, longitude, distanceThreshold); } Future>> _lookupFile(String geohash, double latitude, double longitude, double distanceThreshold) async { @@ -47,7 +54,7 @@ class RugReefer { await _updateLastAccessTime(geohash); } final String geoJsonStr = utf8.decode(GZipCodec().decode(cacheFile.readAsBytesSync())); - return _findMetadata(latitude, longitude, jsonDecode(geoJsonStr), distanceThreshold); + return jsonDecode(geoJsonStr)['features']; } Future>> _lookupWeb(String geohash, double latitude, double longitude, double distanceThreshold) async { @@ -59,12 +66,12 @@ class RugReefer { final geoJsonStr = utf8.decode(response.bodyBytes); await prefs.setString('geojson_$geohash', geoJsonStr); await _updateLastAccessTime(geohash); - return _findMetadata(latitude, longitude, jsonDecode(geoJsonStr), distanceThreshold); + return jsonDecode(geoJsonStr)['features']; } throw Exception('Failed to download GeoJSON for geohash: $geohash'); } await _updateLastAccessTime(geohash); - return _findMetadata(latitude, longitude, jsonDecode(cachedGeoJson), distanceThreshold); + return jsonDecode(cachedGeoJson)['features']; } Future _downloadGeoJson(String geohash, File cacheFile) async { @@ -102,12 +109,20 @@ class RugReefer { } } - List> _findMetadata(double latitude, double longitude, Map geoJson, double distanceThreshold) { + Future> loadJsonFromAssets(String assetPath) async { + final jsonString = await rootBundle.loadString(assetPath); + return jsonDecode(jsonString); + } + + List> _processMetadata(List> features, Map jsonMap, double latitude, double longitude, double distanceThreshold) { final Position point = Position(longitude, latitude); - final List> results = []; - for (var feature in geoJson['features']) { + Map>> groupedObjects = {}; + + for (var feature in features) { final geometry = feature['geometry']; final metadata = Map.from(feature['properties']['tags'] ?? {})..removeWhere((_, v) => v == null); + String? mainKey; + switch (geometry['type']) { case 'Polygon': final polygon = Polygon( @@ -117,7 +132,7 @@ class RugReefer { .toList()) .toList(), ); - if (booleanPointInPolygon(point, polygon)) results.add({"type": "polygon", ...metadata}); + if (booleanPointInPolygon(point, polygon)) mainKey = 'in'; break; case 'LineString': final line = LineString( @@ -125,11 +140,26 @@ class RugReefer { .map((p) => Position(p[0].toDouble(), p[1].toDouble())) .toList(), ); - if (_isPointNearLine(Point(coordinates: point), line, distanceThreshold)) results.add({"type": "line", ...metadata}); + 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 results; + return groupedObjects.entries.map((entry) => {entry.key: entry.value}).toList(); } bool _isPointNearLine(Point point, LineString line, double distanceThreshold) { diff --git a/pubspec.yaml b/pubspec.yaml index 256a09e..2384a6a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,7 +10,6 @@ environment: dependencies: dart_geohash: ^2.1.0 http: ^1.3.0 - latlong2: ^0.9.1 path_provider: ^2.1.5 shared_preferences: ^2.5.2 turf: ^0.0.10