location: use LatLng instead of Tuple for coordinates, approximation when calling geocoder, locate without storing address line
This commit is contained in:
parent
5898c9052a
commit
78f5abc39c
12 changed files with 132 additions and 63 deletions
|
@ -8,13 +8,14 @@ import 'package:aves/services/image_file_service.dart';
|
||||||
import 'package:aves/services/metadata_service.dart';
|
import 'package:aves/services/metadata_service.dart';
|
||||||
import 'package:aves/services/service_policy.dart';
|
import 'package:aves/services/service_policy.dart';
|
||||||
import 'package:aves/utils/change_notifier.dart';
|
import 'package:aves/utils/change_notifier.dart';
|
||||||
|
import 'package:aves/utils/math_utils.dart';
|
||||||
import 'package:aves/utils/time_utils.dart';
|
import 'package:aves/utils/time_utils.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:geocoder/geocoder.dart';
|
import 'package:geocoder/geocoder.dart';
|
||||||
|
import 'package:latlong/latlong.dart';
|
||||||
import 'package:path/path.dart' as ppath;
|
import 'package:path/path.dart' as ppath;
|
||||||
import 'package:tuple/tuple.dart';
|
|
||||||
|
|
||||||
import 'mime_types.dart';
|
import 'mime_types.dart';
|
||||||
|
|
||||||
|
@ -295,9 +296,14 @@ class ImageEntry {
|
||||||
|
|
||||||
bool get isLocated => _addressDetails != null;
|
bool get isLocated => _addressDetails != null;
|
||||||
|
|
||||||
Tuple2<double, double> get latLng => isCatalogued ? Tuple2(_catalogMetadata.latitude, _catalogMetadata.longitude) : null;
|
LatLng get latLng => isCatalogued ? LatLng(_catalogMetadata.latitude, _catalogMetadata.longitude) : null;
|
||||||
|
|
||||||
String get geoUri => hasGps ? 'geo:${_catalogMetadata.latitude},${_catalogMetadata.longitude}?q=${_catalogMetadata.latitude},${_catalogMetadata.longitude}' : null;
|
String get geoUri {
|
||||||
|
if (!hasGps) return null;
|
||||||
|
final latitude = roundToPrecision(_catalogMetadata.latitude, decimals: 6);
|
||||||
|
final longitude = roundToPrecision(_catalogMetadata.longitude, decimals: 6);
|
||||||
|
return 'geo:$latitude,$longitude?q=$latitude,$longitude';
|
||||||
|
}
|
||||||
|
|
||||||
List<String> get xmpSubjects => _catalogMetadata?.xmpSubjects?.split(';')?.where((tag) => tag.isNotEmpty)?.toList() ?? [];
|
List<String> get xmpSubjects => _catalogMetadata?.xmpSubjects?.split(';')?.where((tag) => tag.isNotEmpty)?.toList() ?? [];
|
||||||
|
|
||||||
|
@ -366,7 +372,6 @@ class ImageEntry {
|
||||||
final address = addresses.first;
|
final address = addresses.first;
|
||||||
addressDetails = AddressDetails(
|
addressDetails = AddressDetails(
|
||||||
contentId: contentId,
|
contentId: contentId,
|
||||||
addressLine: address.addressLine,
|
|
||||||
countryCode: address.countryCode,
|
countryCode: address.countryCode,
|
||||||
countryName: address.countryName,
|
countryName: address.countryName,
|
||||||
adminArea: address.adminArea,
|
adminArea: address.adminArea,
|
||||||
|
@ -378,11 +383,29 @@ class ImageEntry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String> findAddressLine() async {
|
||||||
|
final latitude = _catalogMetadata?.latitude;
|
||||||
|
final longitude = _catalogMetadata?.longitude;
|
||||||
|
if (latitude == null || longitude == null || (latitude == 0 && longitude == 0)) return null;
|
||||||
|
|
||||||
|
final coordinates = Coordinates(latitude, longitude);
|
||||||
|
try {
|
||||||
|
final addresses = await Geocoder.local.findAddressesFromCoordinates(coordinates);
|
||||||
|
if (addresses != null && addresses.isNotEmpty) {
|
||||||
|
final address = addresses.first;
|
||||||
|
return address.addressLine;
|
||||||
|
}
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
debugPrint('$runtimeType findAddressLine failed with path=$path coordinates=$coordinates error=$error\n$stackTrace');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
String get shortAddress {
|
String get shortAddress {
|
||||||
if (!isLocated) return '';
|
if (!isLocated) return '';
|
||||||
|
|
||||||
// admin area examples: Seoul, Geneva, null
|
// `admin area` examples: Seoul, Geneva, null
|
||||||
// locality examples: Mapo-gu, Geneva, Annecy
|
// `locality` examples: Mapo-gu, Geneva, Annecy
|
||||||
return {
|
return {
|
||||||
_addressDetails.countryName,
|
_addressDetails.countryName,
|
||||||
_addressDetails.adminArea,
|
_addressDetails.adminArea,
|
||||||
|
@ -390,12 +413,13 @@ class ImageEntry {
|
||||||
}.where((part) => part != null && part.isNotEmpty).join(', ');
|
}.where((part) => part != null && part.isNotEmpty).join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
bool search(String query) {
|
bool search(String query) => {
|
||||||
if (bestTitle?.toUpperCase()?.contains(query) ?? false) return true;
|
bestTitle,
|
||||||
if (_catalogMetadata?.xmpSubjects?.toUpperCase()?.contains(query) ?? false) return true;
|
_catalogMetadata?.xmpSubjects,
|
||||||
if (_addressDetails?.addressLine?.toUpperCase()?.contains(query) ?? false) return true;
|
_addressDetails?.countryName,
|
||||||
return false;
|
_addressDetails?.adminArea,
|
||||||
}
|
_addressDetails?.locality,
|
||||||
|
}.any((s) => s != null && s.toUpperCase().contains(query));
|
||||||
|
|
||||||
Future<void> _applyNewFields(Map newFields) async {
|
Future<void> _applyNewFields(Map newFields) async {
|
||||||
final uri = newFields['uri'];
|
final uri = newFields['uri'];
|
||||||
|
|
|
@ -142,13 +142,12 @@ class OverlayMetadata {
|
||||||
|
|
||||||
class AddressDetails {
|
class AddressDetails {
|
||||||
final int contentId;
|
final int contentId;
|
||||||
final String addressLine, countryCode, countryName, adminArea, locality;
|
final String countryCode, countryName, adminArea, locality;
|
||||||
|
|
||||||
String get place => locality != null && locality.isNotEmpty ? locality : adminArea;
|
String get place => locality != null && locality.isNotEmpty ? locality : adminArea;
|
||||||
|
|
||||||
AddressDetails({
|
AddressDetails({
|
||||||
this.contentId,
|
this.contentId,
|
||||||
this.addressLine,
|
|
||||||
this.countryCode,
|
this.countryCode,
|
||||||
this.countryName,
|
this.countryName,
|
||||||
this.adminArea,
|
this.adminArea,
|
||||||
|
@ -160,7 +159,6 @@ class AddressDetails {
|
||||||
}) {
|
}) {
|
||||||
return AddressDetails(
|
return AddressDetails(
|
||||||
contentId: contentId ?? this.contentId,
|
contentId: contentId ?? this.contentId,
|
||||||
addressLine: addressLine,
|
|
||||||
countryCode: countryCode,
|
countryCode: countryCode,
|
||||||
countryName: countryName,
|
countryName: countryName,
|
||||||
adminArea: adminArea,
|
adminArea: adminArea,
|
||||||
|
@ -171,7 +169,6 @@ class AddressDetails {
|
||||||
factory AddressDetails.fromMap(Map map) {
|
factory AddressDetails.fromMap(Map map) {
|
||||||
return AddressDetails(
|
return AddressDetails(
|
||||||
contentId: map['contentId'],
|
contentId: map['contentId'],
|
||||||
addressLine: map['addressLine'] ?? '',
|
|
||||||
countryCode: map['countryCode'] ?? '',
|
countryCode: map['countryCode'] ?? '',
|
||||||
countryName: map['countryName'] ?? '',
|
countryName: map['countryName'] ?? '',
|
||||||
adminArea: map['adminArea'] ?? '',
|
adminArea: map['adminArea'] ?? '',
|
||||||
|
@ -181,7 +178,6 @@ class AddressDetails {
|
||||||
|
|
||||||
Map<String, dynamic> toMap() => {
|
Map<String, dynamic> toMap() => {
|
||||||
'contentId': contentId,
|
'contentId': contentId,
|
||||||
'addressLine': addressLine,
|
|
||||||
'countryCode': countryCode,
|
'countryCode': countryCode,
|
||||||
'countryName': countryName,
|
'countryName': countryName,
|
||||||
'adminArea': adminArea,
|
'adminArea': adminArea,
|
||||||
|
@ -190,7 +186,7 @@ class AddressDetails {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'AddressDetails{contentId=$contentId, addressLine=$addressLine, countryCode=$countryCode, countryName=$countryName, adminArea=$adminArea, locality=$locality}';
|
return 'AddressDetails{contentId=$contentId, countryCode=$countryCode, countryName=$countryName, adminArea=$adminArea, locality=$locality}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/utils/geo_utils.dart';
|
import 'package:aves/utils/geo_utils.dart';
|
||||||
|
import 'package:latlong/latlong.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
enum CoordinateFormat { dms, decimal }
|
enum CoordinateFormat { dms, decimal }
|
||||||
|
@ -15,12 +16,12 @@ extension ExtraCoordinateFormat on CoordinateFormat {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String format(Tuple2<double, double> latLng) {
|
String format(LatLng latLng) {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case CoordinateFormat.dms:
|
case CoordinateFormat.dms:
|
||||||
return toDMS(latLng).join(', ');
|
return toDMS(latLng).join(', ');
|
||||||
case CoordinateFormat.decimal:
|
case CoordinateFormat.decimal:
|
||||||
return [latLng.item1, latLng.item2].map((n) => n.toStringAsFixed(6)).join(', ');
|
return [latLng.latitude, latLng.longitude].map((n) => n.toStringAsFixed(6)).join(', ');
|
||||||
default:
|
default:
|
||||||
return toString();
|
return toString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/filters/location.dart';
|
import 'package:aves/model/filters/location.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/image_metadata.dart';
|
import 'package:aves/model/image_metadata.dart';
|
||||||
|
@ -30,15 +32,25 @@ mixin LocationMixin on SourceBase {
|
||||||
final todo = byLocated[false] ?? [];
|
final todo = byLocated[false] ?? [];
|
||||||
if (todo.isEmpty) return;
|
if (todo.isEmpty) return;
|
||||||
|
|
||||||
// cache known locations to avoid querying the geocoder unless necessary
|
// geocoder calls take between 150ms and 250ms
|
||||||
// measuring the time it takes to process ~3000 coordinates (with ~10% of duplicates)
|
// approximation and caching can reduce geocoder usage
|
||||||
// does not clearly show whether it is an actual optimization,
|
// for example, for a set of 2932 entries:
|
||||||
// as results vary wildly (durations in "min:sec"):
|
// - 2476 calls (84%) when approximating to 6 decimal places (~10cm - individual humans)
|
||||||
// - with no cache: 06:17, 08:36, 08:34
|
// - 2433 calls (83%) when approximating to 5 decimal places (~1m - individual trees, houses)
|
||||||
// - with cache: 08:28, 05:42, 08:03, 05:58
|
// - 2277 calls (78%) when approximating to 4 decimal places (~10m - individual street, large buildings)
|
||||||
// anyway, in theory it should help!
|
// - 1521 calls (52%) when approximating to 3 decimal places (~100m - neighborhood, street)
|
||||||
final knownLocations = <Tuple2<double, double>, AddressDetails>{};
|
// - 652 calls (22%) when approximating to 2 decimal places (~1km - town or village)
|
||||||
byLocated[true]?.forEach((entry) => knownLocations.putIfAbsent(entry.latLng, () => entry.addressDetails));
|
// cf https://en.wikipedia.org/wiki/Decimal_degrees#Precision
|
||||||
|
final latLngFactor = pow(10, 2);
|
||||||
|
Tuple2 approximateLatLng(ImageEntry entry) {
|
||||||
|
final lat = entry.catalogMetadata?.latitude;
|
||||||
|
final lng = entry.catalogMetadata?.longitude;
|
||||||
|
if (lat == null || lng == null) return null;
|
||||||
|
return Tuple2((lat * latLngFactor).round(), (lng * latLngFactor).round());
|
||||||
|
}
|
||||||
|
|
||||||
|
final knownLocations = <Tuple2, AddressDetails>{};
|
||||||
|
byLocated[true]?.forEach((entry) => knownLocations.putIfAbsent(approximateLatLng(entry), () => entry.addressDetails));
|
||||||
|
|
||||||
var progressDone = 0;
|
var progressDone = 0;
|
||||||
final progressTotal = todo.length;
|
final progressTotal = todo.length;
|
||||||
|
@ -46,13 +58,14 @@ mixin LocationMixin on SourceBase {
|
||||||
|
|
||||||
final newAddresses = <AddressDetails>[];
|
final newAddresses = <AddressDetails>[];
|
||||||
await Future.forEach<ImageEntry>(todo, (entry) async {
|
await Future.forEach<ImageEntry>(todo, (entry) async {
|
||||||
if (knownLocations.containsKey(entry.latLng)) {
|
final latLng = approximateLatLng(entry);
|
||||||
entry.addressDetails = knownLocations[entry.latLng]?.copyWith(contentId: entry.contentId);
|
if (knownLocations.containsKey(latLng)) {
|
||||||
|
entry.addressDetails = knownLocations[latLng]?.copyWith(contentId: entry.contentId);
|
||||||
} else {
|
} else {
|
||||||
await entry.locate(background: true);
|
await entry.locate(background: true);
|
||||||
// it is intended to insert `null` if the geocoder failed,
|
// it is intended to insert `null` if the geocoder failed,
|
||||||
// so that we skip geocoding of following entries with the same coordinates
|
// so that we skip geocoding of following entries with the same coordinates
|
||||||
knownLocations[entry.latLng] = entry.addressDetails;
|
knownLocations[latLng] = entry.addressDetails;
|
||||||
}
|
}
|
||||||
if (entry.isLocated) {
|
if (entry.isLocated) {
|
||||||
newAddresses.add(entry.addressDetails);
|
newAddresses.add(entry.addressDetails);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/painting.dart';
|
import 'package:flutter/painting.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:latlong/latlong.dart';
|
||||||
|
|
||||||
class Constants {
|
class Constants {
|
||||||
// as of Flutter v1.22.3, overflowing `Text` miscalculates height and some text (e.g. 'Å') is clipped
|
// as of Flutter v1.22.3, overflowing `Text` miscalculates height and some text (e.g. 'Å') is clipped
|
||||||
|
@ -21,7 +21,7 @@ class Constants {
|
||||||
static const String overlayUnknown = '—'; // em dash
|
static const String overlayUnknown = '—'; // em dash
|
||||||
static const String infoUnknown = 'unknown';
|
static const String infoUnknown = 'unknown';
|
||||||
|
|
||||||
static const pointNemo = Tuple2(-48.876667, -123.393333);
|
static final pointNemo = LatLng(-48.876667, -123.393333);
|
||||||
|
|
||||||
static const int infoGroupMaxValueLength = 140;
|
static const int infoGroupMaxValueLength = 140;
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
import 'dart:math';
|
import 'package:aves/utils/math_utils.dart';
|
||||||
|
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:latlong/latlong.dart';
|
||||||
|
|
||||||
String _decimal2sexagesimal(final double degDecimal) {
|
String _decimal2sexagesimal(final double degDecimal) {
|
||||||
double _round(final double value, {final int decimals = 6}) => (value * pow(10, decimals)).round() / pow(10, decimals);
|
|
||||||
|
|
||||||
List<int> _split(final double value) {
|
List<int> _split(final double value) {
|
||||||
// NumberFormat is necessary to create digit after comma if the value
|
// NumberFormat is necessary to create digit after comma if the value
|
||||||
// has no decimal point (only necessary for browser)
|
// has no decimal point (only necessary for browser)
|
||||||
final tmp = NumberFormat('0.0#####').format(_round(value, decimals: 10)).split('.');
|
final tmp = NumberFormat('0.0#####').format(roundToPrecision(value, decimals: 10)).split('.');
|
||||||
return <int>[
|
return <int>[
|
||||||
int.parse(tmp[0]).abs(),
|
int.parse(tmp[0]).abs(),
|
||||||
int.parse(tmp[1]),
|
int.parse(tmp[1]),
|
||||||
|
@ -21,14 +18,14 @@ String _decimal2sexagesimal(final double degDecimal) {
|
||||||
final min = _split(minDecimal)[0];
|
final min = _split(minDecimal)[0];
|
||||||
final sec = (minDecimal - min) * 60;
|
final sec = (minDecimal - min) * 60;
|
||||||
|
|
||||||
return '$deg° $min′ ${_round(sec, decimals: 2).toStringAsFixed(2)}″';
|
return '$deg° $min′ ${roundToPrecision(sec, decimals: 2).toStringAsFixed(2)}″';
|
||||||
}
|
}
|
||||||
|
|
||||||
// return coordinates formatted as DMS, e.g. ['41° 24′ 12.2″ N', '2° 10′ 26.5″ E']
|
// return coordinates formatted as DMS, e.g. ['41° 24′ 12.2″ N', '2° 10′ 26.5″ E']
|
||||||
List<String> toDMS(Tuple2<double, double> latLng) {
|
List<String> toDMS(LatLng latLng) {
|
||||||
if (latLng == null) return [];
|
if (latLng == null) return [];
|
||||||
final lat = latLng.item1;
|
final lat = latLng.latitude;
|
||||||
final lng = latLng.item2;
|
final lng = latLng.longitude;
|
||||||
return [
|
return [
|
||||||
'${_decimal2sexagesimal(lat)} ${lat < 0 ? 'S' : 'N'}',
|
'${_decimal2sexagesimal(lat)} ${lat < 0 ? 'S' : 'N'}',
|
||||||
'${_decimal2sexagesimal(lng)} ${lng < 0 ? 'W' : 'E'}',
|
'${_decimal2sexagesimal(lng)} ${lng < 0 ? 'W' : 'E'}',
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
final double _log2 = log(2);
|
final double _log2 = log(2);
|
||||||
const double _piOver180 = pi / 180.0;
|
const double _piOver180 = pi / 180.0;
|
||||||
|
|
||||||
|
@ -9,6 +11,8 @@ double toRadians(num degrees) => degrees * _piOver180;
|
||||||
|
|
||||||
int highestPowerOf2(num x) => x < 1 ? 0 : pow(2, (log(x) / _log2).floor());
|
int highestPowerOf2(num x) => x < 1 ? 0 : pow(2, (log(x) / _log2).floor());
|
||||||
|
|
||||||
|
double roundToPrecision(final double value, {@required final int decimals}) => (value * pow(10, decimals)).round() / pow(10, decimals);
|
||||||
|
|
||||||
// e.g. x=12345, precision=3 should return 13000
|
// e.g. x=12345, precision=3 should return 13000
|
||||||
int ceilBy(num x, int precision) {
|
int ceilBy(num x, int precision) {
|
||||||
final factor = pow(10, precision);
|
final factor = pow(10, precision);
|
||||||
|
|
|
@ -128,7 +128,6 @@ class _DbTabState extends State<DbTab> {
|
||||||
Text('DB address:${data == null ? ' no row' : ''}'),
|
Text('DB address:${data == null ? ' no row' : ''}'),
|
||||||
if (data != null)
|
if (data != null)
|
||||||
InfoRowGroup({
|
InfoRowGroup({
|
||||||
'addressLine': '${data.addressLine}',
|
|
||||||
'countryCode': '${data.countryCode}',
|
'countryCode': '${data.countryCode}',
|
||||||
'countryName': '${data.countryName}',
|
'countryName': '${data.countryName}',
|
||||||
'adminArea': '${data.adminArea}',
|
'adminArea': '${data.adminArea}',
|
||||||
|
|
|
@ -12,6 +12,7 @@ import 'package:aves/widgets/fullscreen/info/maps/google_map.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/maps/leaflet_map.dart';
|
import 'package:aves/widgets/fullscreen/info/maps/leaflet_map.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/maps/marker.dart';
|
import 'package:aves/widgets/fullscreen/info/maps/marker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
class LocationSection extends StatefulWidget {
|
class LocationSection extends StatefulWidget {
|
||||||
final CollectionLens collection;
|
final CollectionLens collection;
|
||||||
|
@ -79,11 +80,9 @@ class _LocationSectionState extends State<LocationSection> with TickerProviderSt
|
||||||
final showMap = (_loadedUri == entry.uri) || (entry.hasGps && widget.visibleNotifier.value);
|
final showMap = (_loadedUri == entry.uri) || (entry.hasGps && widget.visibleNotifier.value);
|
||||||
if (showMap) {
|
if (showMap) {
|
||||||
_loadedUri = entry.uri;
|
_loadedUri = entry.uri;
|
||||||
var location = '';
|
|
||||||
final filters = <LocationFilter>[];
|
final filters = <LocationFilter>[];
|
||||||
if (entry.isLocated) {
|
if (entry.isLocated) {
|
||||||
final address = entry.addressDetails;
|
final address = entry.addressDetails;
|
||||||
location = address.addressLine;
|
|
||||||
final country = address.countryName;
|
final country = address.countryName;
|
||||||
if (country != null && country.isNotEmpty) filters.add(LocationFilter(LocationLevel.country, '$country${LocationFilter.locationSeparator}${address.countryCode}'));
|
if (country != null && country.isNotEmpty) filters.add(LocationFilter(LocationLevel.country, '$country${LocationFilter.locationSeparator}${address.countryCode}'));
|
||||||
final place = address.place;
|
final place = address.place;
|
||||||
|
@ -114,7 +113,8 @@ class _LocationSectionState extends State<LocationSection> with TickerProviderSt
|
||||||
vsync: this,
|
vsync: this,
|
||||||
child: settings.infoMapStyle.isGoogleMaps
|
child: settings.infoMapStyle.isGoogleMaps
|
||||||
? EntryGoogleMap(
|
? EntryGoogleMap(
|
||||||
latLng: entry.latLng,
|
// `LatLng` used by `google_maps_flutter` is not the one from `latlong` package
|
||||||
|
latLng: Tuple2<double, double>(entry.latLng.latitude, entry.latLng.longitude),
|
||||||
geoUri: entry.geoUri,
|
geoUri: entry.geoUri,
|
||||||
initialZoom: settings.infoMapZoom,
|
initialZoom: settings.infoMapZoom,
|
||||||
markerId: entry.uri ?? entry.path,
|
markerId: entry.uri ?? entry.path,
|
||||||
|
@ -130,11 +130,7 @@ class _LocationSectionState extends State<LocationSection> with TickerProviderSt
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (entry.hasGps)
|
if (entry.hasGps) _AddressInfoGroup(entry: entry),
|
||||||
InfoRowGroup(Map.fromEntries([
|
|
||||||
MapEntry('Coordinates', settings.coordinateFormat.format(entry.latLng)),
|
|
||||||
if (location.isNotEmpty) MapEntry('Address', location),
|
|
||||||
])),
|
|
||||||
if (filters.isNotEmpty)
|
if (filters.isNotEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: AvesFilterChip.outlineWidth / 2) + EdgeInsets.only(top: 8),
|
padding: EdgeInsets.symmetric(horizontal: AvesFilterChip.outlineWidth / 2) + EdgeInsets.only(top: 8),
|
||||||
|
@ -160,6 +156,41 @@ class _LocationSectionState extends State<LocationSection> with TickerProviderSt
|
||||||
void _handleChange() => setState(() {});
|
void _handleChange() => setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AddressInfoGroup extends StatefulWidget {
|
||||||
|
final ImageEntry entry;
|
||||||
|
|
||||||
|
const _AddressInfoGroup({@required this.entry});
|
||||||
|
|
||||||
|
@override
|
||||||
|
_AddressInfoGroupState createState() => _AddressInfoGroupState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AddressInfoGroupState extends State<_AddressInfoGroup> {
|
||||||
|
Future<String> _addressLineLoader;
|
||||||
|
|
||||||
|
ImageEntry get entry => widget.entry;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_addressLineLoader = entry.findAddressLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder<String>(
|
||||||
|
future: _addressLineLoader,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final address = !snapshot.hasError && snapshot.connectionState == ConnectionState.done ? snapshot.data : '';
|
||||||
|
return InfoRowGroup({
|
||||||
|
'Coordinates': settings.coordinateFormat.format(entry.latLng),
|
||||||
|
if (address.isNotEmpty) 'Address': address,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// browse providers at https://leaflet-extras.github.io/leaflet-providers/preview/
|
// browse providers at https://leaflet-extras.github.io/leaflet-providers/preview/
|
||||||
enum EntryMapStyle { googleNormal, googleHybrid, googleTerrain, osmHot, stamenToner, stamenWatercolor }
|
enum EntryMapStyle { googleNormal, googleHybrid, googleTerrain, osmHot, stamenToner, stamenWatercolor }
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
import 'package:latlong/latlong.dart';
|
import 'package:latlong/latlong.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../location_section.dart';
|
import '../location_section.dart';
|
||||||
|
@ -18,16 +17,15 @@ class EntryLeafletMap extends StatefulWidget {
|
||||||
final Size markerSize;
|
final Size markerSize;
|
||||||
final WidgetBuilder markerBuilder;
|
final WidgetBuilder markerBuilder;
|
||||||
|
|
||||||
EntryLeafletMap({
|
const EntryLeafletMap({
|
||||||
Key key,
|
Key key,
|
||||||
Tuple2<double, double> latLng,
|
this.latLng,
|
||||||
this.geoUri,
|
this.geoUri,
|
||||||
this.initialZoom,
|
this.initialZoom,
|
||||||
this.style,
|
this.style,
|
||||||
this.markerBuilder,
|
this.markerBuilder,
|
||||||
this.markerSize,
|
this.markerSize,
|
||||||
}) : latLng = LatLng(latLng.item1, latLng.item2),
|
}) : super(key: key);
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => EntryLeafletMapState();
|
State<StatefulWidget> createState() => EntryLeafletMapState();
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import 'package:aves/utils/geo_utils.dart';
|
import 'package:aves/utils/geo_utils.dart';
|
||||||
|
import 'package:latlong/latlong.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('Decimal degrees to DMS (sexagesimal)', () {
|
test('Decimal degrees to DMS (sexagesimal)', () {
|
||||||
expect(toDMS(Tuple2(37.496667, 127.0275)), ['37° 29′ 48.00″ N', '127° 1′ 39.00″ E']); // Gangnam
|
expect(toDMS(LatLng(37.496667, 127.0275)), ['37° 29′ 48.00″ N', '127° 1′ 39.00″ E']); // Gangnam
|
||||||
expect(toDMS(Tuple2(78.9243503, 11.9230465)), ['78° 55′ 27.66″ N', '11° 55′ 22.97″ E']); // Ny-Ålesund
|
expect(toDMS(LatLng(78.9243503, 11.9230465)), ['78° 55′ 27.66″ N', '11° 55′ 22.97″ E']); // Ny-Ålesund
|
||||||
expect(toDMS(Tuple2(-38.6965891, 175.9830047)), ['38° 41′ 47.72″ S', '175° 58′ 58.82″ E']); // Taupo
|
expect(toDMS(LatLng(-38.6965891, 175.9830047)), ['38° 41′ 47.72″ S', '175° 58′ 58.82″ E']); // Taupo
|
||||||
expect(toDMS(Tuple2(-64.249391, -56.6556145)), ['64° 14′ 57.81″ S', '56° 39′ 20.21″ W']); // Marambio
|
expect(toDMS(LatLng(-64.249391, -56.6556145)), ['64° 14′ 57.81″ S', '56° 39′ 20.21″ W']); // Marambio
|
||||||
|
expect(toDMS(LatLng(0, 0)), ['0° 0′ 0.00″ N', '0° 0′ 0.00″ E']);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,11 @@ void main() {
|
||||||
expect(highestPowerOf2(-42), 0);
|
expect(highestPowerOf2(-42), 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('rounding to a given precision after the decimal', () {
|
||||||
|
expect(roundToPrecision(1.2345678, decimals: 3), 1.235);
|
||||||
|
expect(roundToPrecision(0, decimals: 3), 0);
|
||||||
|
});
|
||||||
|
|
||||||
test('rounding up to a given precision before the decimal', () {
|
test('rounding up to a given precision before the decimal', () {
|
||||||
expect(ceilBy(12345.678, 3), 13000);
|
expect(ceilBy(12345.678, 3), 13000);
|
||||||
expect(ceilBy(42, 3), 1000);
|
expect(ceilBy(42, 3), 1000);
|
||||||
|
|
Loading…
Reference in a new issue