test
This commit is contained in:
parent
a5851e71fb
commit
2a093e3126
4 changed files with 149 additions and 20 deletions
|
|
@ -8,21 +8,19 @@ import 'package:turf/turf.dart';
|
||||||
class GeoMetadataLookup {
|
class GeoMetadataLookup {
|
||||||
final String apiUrl;
|
final String apiUrl;
|
||||||
final Directory cacheDir;
|
final Directory cacheDir;
|
||||||
final double distanceThreshold;
|
|
||||||
final Distance distance = const Distance();
|
final Distance distance = const Distance();
|
||||||
|
|
||||||
GeoMetadataLookup({
|
GeoMetadataLookup({
|
||||||
required this.apiUrl,
|
required this.apiUrl,
|
||||||
required this.cacheDir,
|
required this.cacheDir,
|
||||||
this.distanceThreshold = 10.0, // Default 10 meters
|
|
||||||
}) {
|
}) {
|
||||||
if (!cacheDir.existsSync()) {
|
if (!cacheDir.existsSync()) {
|
||||||
cacheDir.createSync(recursive: true);
|
cacheDir.createSync(recursive: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Map<String, dynamic>>> lookup(double latitude, double longitude) async {
|
Future<List<Map<String, dynamic>>> lookup(double latitude, double longitude, {double distance = 10.0}) async {
|
||||||
final String geohash = GeoHasher().encode(latitude, longitude, precision: 4);
|
final String geohash = GeoHasher().encode(longitude, latitude);
|
||||||
final File cacheFile = File('${cacheDir.path}/$geohash.geojson.gz');
|
final File cacheFile = File('${cacheDir.path}/$geohash.geojson.gz');
|
||||||
|
|
||||||
if (!cacheFile.existsSync()) {
|
if (!cacheFile.existsSync()) {
|
||||||
|
|
@ -31,11 +29,11 @@ class GeoMetadataLookup {
|
||||||
|
|
||||||
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);
|
final Map<String, dynamic> geoJson = jsonDecode(geoJsonStr);
|
||||||
return _findMetadata(latitude, longitude, geoJson);
|
return _findMetadata(latitude, longitude, distance, geoJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _downloadGeoJson(String geohash, File cacheFile) async {
|
Future<void> _downloadGeoJson(String geohash, File cacheFile) async {
|
||||||
final response = await http.get(Uri.parse('$apiUrl/$geohash.geojson.gz'));
|
final response = await http.get(Uri.parse('$apiUrl$geohash'));
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
cacheFile.writeAsBytesSync(GZipCodec().encode(response.bodyBytes));
|
cacheFile.writeAsBytesSync(GZipCodec().encode(response.bodyBytes));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -43,8 +41,8 @@ class GeoMetadataLookup {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Map<String, dynamic>> _findMetadata(double latitude, double longitude, Map<String, dynamic> geoJson) {
|
List<Map<String, dynamic>> _findMetadata(double latitude, double longitude, double distanceThreshold, Map<String, dynamic> geoJson) {
|
||||||
final Point point = Point(coordinates: Position(longitude, latitude));
|
final Position point = Position(longitude, latitude);
|
||||||
List<Map<String, dynamic>> results = [];
|
List<Map<String, dynamic>> results = [];
|
||||||
|
|
||||||
for (var feature in geoJson['features']) {
|
for (var feature in geoJson['features']) {
|
||||||
|
|
@ -59,19 +57,30 @@ class GeoMetadataLookup {
|
||||||
.toList())
|
.toList())
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
|
if (booleanPointInPolygon(point, polygon)) {
|
||||||
|
results.add({'type': 'polygon', ...properties});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (geometry['type'] == 'LineString') {
|
else if (geometry['type'] == 'LineString') {
|
||||||
final line = LineString(coordinates: (geometry['coordinates'] as List).map((p) => Position(p[0], p[1])).toList());
|
final line = LineString(
|
||||||
if (_isPointNearLine(point, line)) {
|
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', ...properties});
|
results.add({'type': 'line', ...properties});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (geometry['type'] == 'MultiLineString') {
|
else if (geometry['type'] == 'MultiLineString') {
|
||||||
for (var lineCoords in geometry['coordinates']) {
|
for (var lineCoords in geometry['coordinates']) {
|
||||||
final line = LineString(coordinates: lineCoords.map((p) => Position(p[0], p[1])).toList());
|
final line = LineString(
|
||||||
if (_isPointNearLine(point, line)) {
|
coordinates: (lineCoords as List)
|
||||||
|
.map((p) => Position(p[0] as double, p[1] as double))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
if (_isPointNearLine(point, line, distanceThreshold)) {
|
||||||
results.add({'type': 'multi_line', ...properties});
|
results.add({'type': 'multi_line', ...properties});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -81,12 +90,11 @@ class GeoMetadataLookup {
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isPointNearLine(Point point, LineString line) {
|
bool _isPointNearLine(Position point, LineString line, double distanceThreshold) {
|
||||||
for (var i = 0; i < line.coordinates.length - 1; i++) {
|
for (var i = 0; i < line.coordinates.length - 1; i++) {
|
||||||
final p1 = line.coordinates[i];
|
final p1 = line.coordinates[i];
|
||||||
final p2 = line.coordinates[i + 1];
|
|
||||||
final double dist = distance.distance(
|
final double dist = distance.distance(
|
||||||
LatLng(point.coordinates.lat.toDouble(), point.coordinates.lng.toDouble()),
|
LatLng(point.lat.toDouble(), point.lng.toDouble()),
|
||||||
LatLng(p1.lat.toDouble(), p1.lng.toDouble()),
|
LatLng(p1.lat.toDouble(), p1.lng.toDouble()),
|
||||||
);
|
);
|
||||||
if (dist <= distanceThreshold) return true;
|
if (dist <= distanceThreshold) return true;
|
||||||
|
|
|
||||||
10
pubspec.lock
10
pubspec.lock
|
|
@ -241,6 +241,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
mocktail:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: mocktail
|
||||||
|
sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
node_preamble:
|
node_preamble:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -386,7 +394,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.2"
|
version: "1.2.2"
|
||||||
test:
|
test:
|
||||||
dependency: "direct dev"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: test
|
name: test
|
||||||
sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e"
|
sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e"
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,10 @@ dependencies:
|
||||||
dart_geohash: ^2.1.0
|
dart_geohash: ^2.1.0
|
||||||
http: ^1.3.0
|
http: ^1.3.0
|
||||||
latlong2: ^0.9.1
|
latlong2: ^0.9.1
|
||||||
|
mocktail: ^1.0.4
|
||||||
|
test: ^1.25.15
|
||||||
turf: ^0.0.10
|
turf: ^0.0.10
|
||||||
# path: ^1.8.0
|
# path: ^1.8.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
lints: ^5.0.0
|
lints: ^5.0.0
|
||||||
test: ^1.24.0
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,117 @@
|
||||||
import 'package:rugreefer/rugreefer.dart'; // Ensure this import is correct
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:rugreefer/rugreefer.dart'; // Replace with actual package name
|
||||||
|
|
||||||
|
class MockClient extends Mock implements http.Client {}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
print('Hello world!'); // Remove the missing method call
|
late GeoMetadataLookup lookup;
|
||||||
}
|
late Directory cacheDir;
|
||||||
|
late MockClient mockHttpClient;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
cacheDir = Directory.systemTemp.createTempSync();
|
||||||
|
mockHttpClient = MockClient();
|
||||||
|
lookup = GeoMetadataLookup(
|
||||||
|
apiUrl: 'http://localhost:8000/get_geodata/?format=geojson&geohash=',
|
||||||
|
cacheDir: cacheDir,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
cacheDir.deleteSync(recursive: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Fetches and correctly parses GeoJSON with a residential landuse feature', () async {
|
||||||
|
final String geohash = 'sv2m';
|
||||||
|
final File cacheFile = File('${cacheDir.path}/$geohash.geojson.gz');
|
||||||
|
|
||||||
|
final Map<String, dynamic> mockGeoJson = {
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"id": "0",
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": {
|
||||||
|
"feature_id": "way/579610585",
|
||||||
|
"tags": {"waterway": "stream"}
|
||||||
|
},
|
||||||
|
"geometry": {
|
||||||
|
"type": "LineString",
|
||||||
|
"coordinates": [
|
||||||
|
[34.453125, 30.5735040],
|
||||||
|
[34.4515371, 30.5729953],
|
||||||
|
[34.4507646, 30.5725335]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
final mockResponse = utf8.encode(jsonEncode(mockGeoJson));
|
||||||
|
cacheFile.writeAsBytesSync(GZipCodec().encode(mockResponse));
|
||||||
|
|
||||||
|
final results = await lookup.lookup(32.827714492634676, 34.96983862139979);
|
||||||
|
|
||||||
|
String prettyJson = const JsonEncoder.withIndent(' ').convert(results);
|
||||||
|
print(prettyJson);
|
||||||
|
|
||||||
|
expect(results, isNotEmpty);
|
||||||
|
expect(results.any((r) => r['type'] == 'polygon' && r['tags']['landuse'] == 'residential'), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Ensures caching of GeoJSON data', () async {
|
||||||
|
final String geohash = 'sv2m';
|
||||||
|
final File cacheFile = File('${cacheDir.path}/$geohash.geojson.gz');
|
||||||
|
|
||||||
|
final mockGeoJson = jsonEncode({
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": []
|
||||||
|
});
|
||||||
|
|
||||||
|
final mockResponse = utf8.encode(mockGeoJson);
|
||||||
|
cacheFile.writeAsBytesSync(GZipCodec().encode(mockResponse));
|
||||||
|
|
||||||
|
expect(cacheFile.existsSync(), isTrue);
|
||||||
|
|
||||||
|
await lookup.lookup(30.5730, 34.4520);
|
||||||
|
expect(cacheFile.existsSync(), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Handles API call correctly when file is not cached', () async {
|
||||||
|
final String geohash = 'sv2m';
|
||||||
|
|
||||||
|
when(() => mockHttpClient.get(Uri.parse('http://localhost:8000/get_geodata/?format=geojson&geohash=$geohash')))
|
||||||
|
.thenAnswer((_) async => http.Response(jsonEncode({
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"id": "0",
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": {
|
||||||
|
"feature_id": "way/579610585",
|
||||||
|
"tags": {"waterway": "stream"}
|
||||||
|
},
|
||||||
|
"geometry": {
|
||||||
|
"type": "LineString",
|
||||||
|
"coordinates": [
|
||||||
|
[34.453125, 30.5735040],
|
||||||
|
[34.4515371, 30.5729953]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}), 200));
|
||||||
|
|
||||||
|
final results = await lookup.lookup(31.776673242005337, 35.23441728409178);
|
||||||
|
|
||||||
|
String prettyJson = const JsonEncoder.withIndent(' ').convert(results);
|
||||||
|
print(prettyJson);
|
||||||
|
|
||||||
|
expect(results, isNotEmpty);
|
||||||
|
expect(results.any((r) => r['type'] == 'polygon' && r['tags']['historic'] == 'heritage'), isTrue);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue