前書き
この記事は、大学2年の春休みにインプット力とアウトプット力を鍛えるべく毎日記事を投稿する試みの第1本目の記事です。
筆者は記事投稿経験がほぼ無く、プログラミング歴も2年ほどと浅いです。ですので間違い等を発見された場合は優しくご指摘ください。
はじめに
先日でたハッカソンにて、新たな歴史的建造物と出会えるアプリを開発しました。
そのミニゲーム兼目玉として実装したリアルジオゲッサーのような機能について解説したいと思います。
前提
今回はデータベースに画像とその場所の住所が入ったデータを事前に用意してある前提ですすめます。
その際住所には番地まで記入しておいてください。
使うパッケージ
今回使うパッケージは主に2つで、現在地を取得するために使うgeolocatorと、
それを住所に変換するgeocodingです。
機能作成
まず初めに、現在地を取得するコードを先ほどのgeolocatorを使って記述します。
また、位置情報取得には権限がいるのでiosとandroidの設定ファイルにそれぞれ以下のコードを追加してください。
ios
ios/Runner/info.pslist
<!-- geolocator Section -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>Required to post location information when posting photos and videos.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Required to post location information when posting photos and videos.</string>
<!-- End of the geolocator Section -->
Android
android/app/src/main/AndroidManifest.xml
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
この設定がおわれば、
lib/utils/locationなどのフォルダに新規dartファイルを作成し、下記のコードを記述。
get_location.dart
import 'package:geolocator/geolocator.dart';
Future<Position> _determinePosition() async {
bool serviceEnabled;
LocationPermission permission;
// Test if location services are enabled.
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
return Future.error('Location services are disabled.');
}
permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
return Future.error('Location permissions are denied');
}
}
if (permission == LocationPermission.deniedForever) {
return Future.error(
'Location permissions are permanently denied, we cannot request permissions.');
}
return await Geolocator.getCurrentPosition();
}
単純にパッケージサイトからコピペするだけです。
これで現在地を取得するコードができました。
続いて問題として出題するデータの住所からロケーションに変換するためgeocodingを用います。
もしもデータベースに緯度経度を保存しているなら不要です。
同じフォルダにファイルを作成し、以下のコードを記述。
translate_location.dart
import 'package:geocoding/geocoding.dart';
import 'package:geolocator/geolocator.dart';
Future<Placemark> transLateLocateToAddress(Position location) async {
List<Placemark> placeMarks = await placemarkFromCoordinates(
location.latitude,
location.longitude,
);
final placeMark = placeMarks[0];
return placeMark;
}
Future<Location> transLateAddressToLocate(String address) async {
final locations = await locationFromAddress(address);
return locations.first;
}
transLateLocateToAddressは実際のアプリでは必要ありませんが、デバッグの際などに役に立ちます。
最後に得点を計算するコードを記述します。
2点間の距離を測定するメソッドは既にgeolocationにあるのでそれを利用します。
新しくファイルcalc_score.dartに以下記述。
import 'package:geoguessur_test/utils/location_info/get_location_info.dart';
import 'package:geolocator/geolocator.dart';
import 'package:geoguessur_test/utils/location_info/translate_location.dart';
Future<int> calculateScore(
String address,
double maxDistance,
Position location,
) async {
try {
final quizeLocation = await transLateAddressToLocate(address);
final distance = await Geolocator.distanceBetween(
location.latitude,
location.longitude,
quizeLocation.latitude,
quizeLocation.longitude,
);
// 距離に基づいて点数を計算
final score =
((maxDistance - distance) / maxDistance * 100).clamp(0, 100).toInt();
if (score < 0) {
return 0;
} else if (score > 95) {
return 100;
}
return score;
} catch (e) {
throw Exception('Error calculating score: $e');
}
}
これで機能作成は完了しました。
画面での使用
クイズ画面の回答ボタン部に以下のような処理で実装します。(go_routerは割愛)
GestureDetector(
onTap: () async {
try {
final answerLocation = await getCurrentPosition();
final score = await calculateScore(
placeFuture.data!.address,
maxDistance,
answerLocation,
);
context.go(
'./result',
extra: (score, placeFuture.data),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e')),
);
}
おまけ
今回つくったアプリはレベルによって難易度を変えていました。レベルの一つが現在の都道府県の中から出題するというもので、その際もgeocodingが役に立ちました。
実際のコードはこのようになっています。(Placeは独自に定義しています)
import 'dart:math';
import 'package:geoguessur_test/interface/place.dart';
import 'package:geoguessur_test/service/database/firestore_service.dart';
import 'package:geoguessur_test/utils/location_info/translate_location.dart';
import 'package:geolocator/geolocator.dart';
class GeoService {
final FirestoreService _firestoreService = FirestoreService();
Future<Place> getRandomPlace(int level, Position location) async {
final currentAddress = await transLateLocateToAddress(location);
final prefecture = currentAddress.administrativeArea;
if (prefecture == null) {
throw Exception('Failed to get prefecture');
}
print(prefecture);
// Firestoreからデータを取得
List<Place> places = await _firestoreService.getAllPlaces();
// レベルによって絞り込み
List<Place> filteredPlaces;
if (level == 3) {
filteredPlaces = places;
} else if (level == 2) {
filteredPlaces =
places.where((place) => place.address.contains(prefecture)).toList();
} else {
filteredPlaces = places.where((place) => place.popularity == 5).toList();
}
// ランダムに一つのPlaceを選択
Random random = Random();
int randomIndex = random.nextInt(filteredPlaces.length);
Place selectedPlace = filteredPlaces[randomIndex];
return selectedPlace;
}
}
それを先程の出題画面で
Future<Place> _fetchPlace(GeoService geoService, int level) async {
final location = await getCurrentPosition();
return await geoService.getRandomPlace(level, location);
}
Widget build(BuildContext context) {
final geoService = GeoService();
final maxDistance = 50000.0; // 大阪府の端から端までの長さの半分(メートル)
final placeData = useMemoized(() => _fetchPlace(geoService, level));
final placeFuture = useFuture(placeData)
という感じで使いました。細かいがめんなどはまた機会があれば紹介します。
参考記事