mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-02 06:46:22 +01:00
Add reverse geocoder
This commit is contained in:
parent
94c7e420d1
commit
093bc6e327
5 changed files with 208 additions and 3 deletions
BIN
app/assets/cities.sqlite
Normal file
BIN
app/assets/cities.sqlite
Normal file
Binary file not shown.
|
@ -2,9 +2,11 @@ import 'dart:io' as dart;
|
|||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
import 'package:path/path.dart' as path_lib;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart' as sqlite3;
|
||||
import 'package:sqlite3/sqlite3.dart';
|
||||
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart' as sql;
|
||||
|
||||
Future<Map<String, dynamic>> getSqliteConnectionArgs() async {
|
||||
// put the database file, called db.sqlite here, into the documents folder
|
||||
|
@ -32,5 +34,27 @@ QueryExecutor openSqliteConnection() {
|
|||
}
|
||||
|
||||
Future<void> applyWorkaroundToOpenSqlite3OnOldAndroidVersions() {
|
||||
return sqlite3.applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
|
||||
return sql.applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
|
||||
}
|
||||
|
||||
Future<Database> openRawSqliteDbFromAsset(
|
||||
String assetRelativePath,
|
||||
String outputFilename, {
|
||||
bool isReadOnly = false,
|
||||
}) async {
|
||||
final dbFolder = await getApplicationDocumentsDirectory();
|
||||
final file = dart.File(path_lib.join(dbFolder.path, outputFilename));
|
||||
if (!await file.exists()) {
|
||||
// copy file from assets
|
||||
final blob = await rootBundle.load("assets/$assetRelativePath");
|
||||
final buffer = blob.buffer;
|
||||
await file.writeAsBytes(
|
||||
buffer.asUint8List(blob.offsetInBytes, blob.lengthInBytes),
|
||||
flush: true,
|
||||
);
|
||||
}
|
||||
return sqlite3.open(
|
||||
file.path,
|
||||
mode: isReadOnly ? OpenMode.readOnly : OpenMode.readWriteCreate,
|
||||
);
|
||||
}
|
||||
|
|
172
app/lib/reverse_geocoder.dart
Normal file
172
app/lib/reverse_geocoder.dart
Normal file
|
@ -0,0 +1,172 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:kdtree/kdtree.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/math_util.dart' as math_util;
|
||||
import 'package:nc_photos/mobile/platform.dart'
|
||||
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
|
||||
import 'package:sqlite3/common.dart';
|
||||
|
||||
class ReverseGeocoderLocation {
|
||||
const ReverseGeocoderLocation(this.name, this.latitude, this.longitude,
|
||||
this.countryCode, this.admin1, this.admin2);
|
||||
|
||||
@override
|
||||
toString() => "$runtimeType {"
|
||||
"name: $name, "
|
||||
"latitude: $latitude, "
|
||||
"longitude: $longitude, "
|
||||
"countryCode: $countryCode, "
|
||||
"admin1: $admin1, "
|
||||
"admin2: $admin2, "
|
||||
"}";
|
||||
|
||||
final String name;
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
final String countryCode;
|
||||
final String? admin1;
|
||||
final String? admin2;
|
||||
}
|
||||
|
||||
class ReverseGeocoder {
|
||||
Future<void> init() async {
|
||||
final s = Stopwatch()..start();
|
||||
_db = await _openDatabase();
|
||||
_searchTree = _buildSearchTree(_db);
|
||||
_log.info("[init] Elapsed time: ${s.elapsedMilliseconds}ms");
|
||||
}
|
||||
|
||||
/// Convert a geographic coordinate (in degree) into a location
|
||||
Future<ReverseGeocoderLocation?> call(
|
||||
double latitude, double longitude) async {
|
||||
_log.info(
|
||||
"[call] latitude: ${latitude.toStringAsFixed(3)}, longitude: ${longitude.toStringAsFixed(3)}");
|
||||
final latitudeInt = (latitude * 10000).round();
|
||||
final longitudeInt = (longitude * 10000).round();
|
||||
final nearest = _searchTree
|
||||
.nearest({"t": latitudeInt, "g": longitudeInt}, 1).firstOrNull;
|
||||
if (nearest == null) {
|
||||
_log.info("[call] Nearest point not found");
|
||||
return null;
|
||||
}
|
||||
final nearestLat = nearest[0]["t"];
|
||||
final nearestLatF = nearestLat / 10000;
|
||||
final nearestLng = nearest[0]["g"];
|
||||
final nearestLngF = nearestLng / 10000;
|
||||
_log.info("[call] Nearest point, (lat: $nearestLatF, lng: $nearestLngF)");
|
||||
try {
|
||||
final distance = _distanceInKm(
|
||||
math_util.degToRad(latitude),
|
||||
math_util.degToRad(longitude),
|
||||
math_util.degToRad(nearestLatF),
|
||||
math_util.degToRad(nearestLngF),
|
||||
);
|
||||
_log.info(
|
||||
"[call] (lat: ${latitude.toStringAsFixed(3)}, lng: ${longitude.toStringAsFixed(3)}) <-> (lat: $nearestLatF, lng: $nearestLngF) = ${distance.toStringAsFixed(3)}km");
|
||||
// a completely arbitrary threshold :)
|
||||
if (distance > 10) {
|
||||
_log.info("[call] Nearest point is too far away");
|
||||
return null;
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[call] Uncaught exception", e, stackTrace);
|
||||
}
|
||||
|
||||
final data = _queryPoint(nearestLat, nearestLng);
|
||||
if (data == null) {
|
||||
_log.severe(
|
||||
"[call] Row not found for point: latitude: $nearestLat, longitude: $nearestLng");
|
||||
return null;
|
||||
}
|
||||
final result = ReverseGeocoderLocation(data.name, data.latitude / 10000,
|
||||
data.longitude / 10000, data.countryCode, data.admin1, data.admin2);
|
||||
_log.info("[call] Found: $result");
|
||||
return result;
|
||||
}
|
||||
|
||||
_DatabaseRow? _queryPoint(int latitudeInt, int longitudeInt) {
|
||||
final result = _db.select(
|
||||
"SELECT * FROM cities WHERE latitude = ? AND longitude = ? LIMIT 1;",
|
||||
[latitudeInt, longitudeInt],
|
||||
);
|
||||
if (result.isEmpty) {
|
||||
return null;
|
||||
} else {
|
||||
return _DatabaseRow(
|
||||
result.first.columnAt(1),
|
||||
result.first.columnAt(2),
|
||||
result.first.columnAt(3),
|
||||
result.first.columnAt(4),
|
||||
result.first.columnAt(5),
|
||||
result.first.columnAt(6),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
late final CommonDatabase _db;
|
||||
late final KDTree _searchTree;
|
||||
|
||||
static final _log = Logger("reverse_geocoder.ReverseGeocoder");
|
||||
}
|
||||
|
||||
extension ReverseGeocoderExtension on ReverseGeocoderLocation {
|
||||
ImageLocation toImageLocation() {
|
||||
return ImageLocation(
|
||||
name: name,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
countryCode: countryCode,
|
||||
admin1: admin1,
|
||||
admin2: admin2,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DatabaseRow {
|
||||
const _DatabaseRow(this.name, this.latitude, this.longitude, this.countryCode,
|
||||
this.admin1, this.admin2);
|
||||
|
||||
final String name;
|
||||
final int latitude;
|
||||
final int longitude;
|
||||
final String countryCode;
|
||||
final String? admin1;
|
||||
final String? admin2;
|
||||
}
|
||||
|
||||
Future<CommonDatabase> _openDatabase() async {
|
||||
return platform.openRawSqliteDbFromAsset("cities.sqlite", "cities.sqlite");
|
||||
}
|
||||
|
||||
KDTree _buildSearchTree(CommonDatabase db) {
|
||||
final results = db.select("SELECT latitude, longitude FROM cities;");
|
||||
return KDTree(
|
||||
results.map((e) => {"t": e.columnAt(0), "g": e.columnAt(1)}).toList(),
|
||||
_kdTreeDistance,
|
||||
["t", "g"],
|
||||
);
|
||||
}
|
||||
|
||||
int _kdTreeDistance(Map a, Map b) {
|
||||
return (math.pow((a["t"] as int) - (b["t"] as int), 2) +
|
||||
math.pow((a["g"] as int) - (b["g"] as int), 2)) as int;
|
||||
}
|
||||
|
||||
/// Calculate the distance in KM between two point
|
||||
///
|
||||
/// Both latitude and longitude are expected to be in radian
|
||||
double _distanceInKm(
|
||||
double latitude1, double longitude1, double latitude2, double longitude2) {
|
||||
final dLat = latitude2 - latitude1;
|
||||
final dLon = longitude2 - longitude1;
|
||||
final a = math.pow(math.sin(dLat / 2), 2) +
|
||||
math.cos(latitude1) *
|
||||
math.cos(latitude2) *
|
||||
math.pow(math.sin(dLon / 2), 2);
|
||||
final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
|
||||
// 6371 = earth radius
|
||||
return 6371 * c;
|
||||
}
|
|
@ -679,6 +679,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.6.0"
|
||||
kdtree:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: kdtree
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
kiwi:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1161,7 +1168,7 @@ packages:
|
|||
source: hosted
|
||||
version: "2.2.1+1"
|
||||
sqlite3:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqlite3
|
||||
url: "https://pub.dartlang.org"
|
||||
|
|
|
@ -76,6 +76,7 @@ dependencies:
|
|||
ref: 1.0.0-nc-photos-2
|
||||
path: library
|
||||
intl: ^0.17.0
|
||||
kdtree: ^0.2.0
|
||||
kiwi: ^4.0.1
|
||||
logging: ^1.0.1
|
||||
memory_info: ^0.0.2
|
||||
|
@ -90,6 +91,7 @@ dependencies:
|
|||
quiver: ^3.1.0
|
||||
screen_brightness: ^0.2.1
|
||||
shared_preferences: ^2.0.8
|
||||
sqlite3: any
|
||||
sqlite3_flutter_libs: ^0.5.8
|
||||
synchronized: ^3.0.0
|
||||
tuple: ^2.0.0
|
||||
|
|
Loading…
Reference in a new issue