【Flutter】geoflutterfireという範囲内の座標のみを取得する神パッケージの紹介
geoflutterfireとは?
皆さん、geoflutterfireという神パッケージをご存知だろうか。。。
geoflutterfireを用いることで以下のように範囲内の座標のみをFirestoreから取得できるようになるのである。(円はだいたいの目安です)
今回はgeoflutterfireに関する日本語記事がほとんど見当たらなかったためここに記す。
最後にサンプルコードも置いとくよ!!
geoflutterfireの使い方
こちらのドキュメントによるとgeoflutterfireの特徴は以下になります!
- 地理的な位置(座標)を文字列(GeoHash)として保存する
- エリア内の座標をリアルタイムで取得できる
- 特定の場所に近いデータのみをロードするため、非常に大きなデータセットでも軽く動かせる。
具体的にどのようにできるのか見ていきましょう。
前提
Firebaseの接続及びFirestoreを使用できる状態にする
初期化
import 'package:geoflutterfire/geoflutterfire.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
// Init firestore and geoFlutterFire
final geo = Geoflutterfire();
final _firestore = FirebaseFirestore.instance;
地理情報を書きこむ
GeoFirePoint
を使用する
GeoFirePoint geoFirePoint = geo.point(latitude: 12.960632, longitude: 77.641603);
GeoFirePoint
とは
地理情報を良い感じに扱ってるクラス。3つのgetter(privateになってる変数)がある。
-
geoFirePoint.hash
: GeoHashという9文字の文字列を返す。geohashについては後述します。 例:s1b8yu2nj -
geoFirePoint.geoPoint
: Firestoreで使われるGeoPointを返す。GeoPoint (double latitude, double longitude)
緯度経度を持つ。 -
geoFirePoint.data
: Firestoreへの保存に適したデータを返す。例:{geopoint: Instance of 'GeoPoint', geohash: s1b8yu2nj}
GeoFirePoint
をFirestoreに追加する
_firestore
.collection('shop')
.add({'name': 'random name', 'position': geoFirePoint.data});
実際にはこんな感じでFirestoreに保存されます
地理情報を取得する
ある地点から50km以内のドキュメントを取得するクエリ。Streamで値を受け取る。
// Create a geoFirePoint
GeoFirePoint center = geo.point(latitude: 12.960632, longitude: 77.641603);
// get the collection reference or query
var collectionReference = _firestore.collection('shop');
double radius = 50;
String field = 'position';
Stream<List<DocumentSnapshot>> stream =
geo.collection(collectionRef: collectionReference)
.within(center: center, radius: radius, field: field);
受け取ったStreamを監視。
stream.listen((List<DocumentSnapshot> documentList) {
// doSomething()
});
Geohashとは
Geohash
について書かれていました!
Geohash
について一部引用させてもらいます。
Geohashは、地図を格子状に分割し、その1区画を短い文字列で表現できるというものです。 例えば、東京駅周辺はGeohash6桁では「xn76ur」と表現されます。
近隣の区画は似た文字列で表現できることから、あるGeohashの区画の隣の区画は簡単に計算で求めることができます。
簡単に説明すると Geohash
は
- 座標を文字列で表現
- 桁数が大きいほど精度が高くなる
- より多くの文字列が一致すれば、2点がより近いことを表す。
という特徴を持っています。
geoflutterfireでは9桁の文字列として返ってくるので精度は高いです。
※Yahoo!さんのテックブログから引用
geoflutterfireを使ってみる
こちらのサンプルコードを最後に置いておきます!
準備
Firebaseの接続及びfirestoreを使用できるようにする。
FirestoreにgeoFlutterPointを追加する
GoogleMapを使えるようにする
使用しているライブラリ
dependencies:
google_maps_flutter: ^2.1.2
geolocator: ^8.2.0
firebase_core: ^1.13.1
cloud_firestore: ^3.1.10
geoflutterfire: ^3.0.3
サンプルコード
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:geoflutterfire/geoflutterfire.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:rxdart/rxdart.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(
MaterialApp(
title: 'Geo Flutter Fire example',
home: MyApp(),
debugShowCheckedModeBanner: true,
),
);
}
class MyApp extends StatefulWidget {
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
GoogleMapController? _mapController;
TextEditingController? _latitudeController, _longitudeController;
// firestore init
final radius = BehaviorSubject<double>.seeded(1.0);
final _firestore = FirebaseFirestore.instance;
final markers = <MarkerId, Marker>{};
late Stream<List<DocumentSnapshot>> stream;
late Geoflutterfire geo;
double _value = 20.0;
String _label = '';
double _ratio = 0;
double screenWidthKms = 600;
void initState() {
super.initState();
_latitudeController = TextEditingController();
_longitudeController = TextEditingController();
geo = Geoflutterfire();
GeoFirePoint center =
geo.point(latitude: 43.0779575, longitude: 142.337819);
stream = radius.switchMap(
(rad) {
final collectionReference = _firestore.collection('shop');
return geo.collection(collectionRef: collectionReference).within(
center: center, radius: rad, field: 'position', strictMode: true);
},
);
Future(() async {
//_mapControllerがinitializeされるのを待つ1秒
await Future.delayed(const Duration(seconds: 1));
final region = await _mapController?.getVisibleRegion();
final distanceInMeters = Geolocator.distanceBetween(
region!.northeast.latitude,
region.northeast.longitude,
region.southwest.latitude,
region.northeast.longitude);
screenWidthKms = distanceInMeters / 1000;
print('画面の横幅の距離 $screenWidthKms km');
});
}
void dispose() {
_latitudeController?.dispose();
_longitudeController?.dispose();
radius.close();
super.dispose();
}
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Center(
child: Card(
elevation: 4,
margin: const EdgeInsets.symmetric(vertical: 8),
child: SizedBox(
height: 550,
child: Stack(
children: [
GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: const CameraPosition(
target: LatLng(43.0779575, 142.337819),
zoom: 6.5,
),
markers: Set<Marker>.of(markers.values),
),
Center(
child: Container(
width: MediaQuery.of(context).size.width * (_ratio),
height: MediaQuery.of(context).size.width * (_ratio),
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.5),
shape: BoxShape.circle,
),
),
),
],
),
),
),
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Slider(
min: 1,
max: screenWidthKms / 2,
divisions: 10,
value: _value,
label: _label,
activeColor: Colors.blue,
inactiveColor: Colors.blue.withOpacity(0.2),
onChanged: (double value) {
setState(() {
_value = value;
_label = '${_value.toInt().toString()} kms';
_ratio = _value / (screenWidthKms / 2);
markers.clear();
});
radius.add(value);
},
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
SizedBox(
width: 100,
child: TextField(
controller: _latitudeController,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
decoration: InputDecoration(
labelText: 'lat',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
SizedBox(
width: 100,
child: TextField(
controller: _longitudeController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'lng',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
)),
),
),
MaterialButton(
color: Colors.blue,
onPressed: () {
final lat =
double.parse(_latitudeController?.text ?? '0.0');
final lng =
double.parse(_longitudeController?.text ?? '0.0');
_addPoint(lat, lng);
},
child: const Text(
'ADD',
style: TextStyle(color: Colors.white),
),
),
],
),
],
),
),
);
}
void _onMapCreated(GoogleMapController controller) async {
setState(() {
_mapController = controller;
//start listening after map is created
stream.listen((List<DocumentSnapshot> documentList) {
_updateMarkers(documentList);
});
});
}
void _addPoint(double lat, double lng) {
GeoFirePoint geoFirePoint = geo.point(latitude: lat, longitude: lng);
print(geoFirePoint.hash);
print(geoFirePoint.geoPoint);
print(geoFirePoint.data);
print(geoFirePoint);
_firestore
.collection('shop')
.add({'name': 'random name', 'position': geoFirePoint.data}).then((_) {
print('added ${geoFirePoint.hash} successfully');
});
}
void _addMarker(double lat, double lng) {
final id = MarkerId(lat.toString() + lng.toString());
final _marker = Marker(
markerId: id,
position: LatLng(lat, lng),
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueViolet),
infoWindow: InfoWindow(title: 'latLng', snippet: '$lat,$lng'),
);
setState(() {
markers[id] = _marker;
});
}
void _updateMarkers(List<DocumentSnapshot> documentList) {
documentList.forEach((DocumentSnapshot document) {
final data = document.data() as Map<String, dynamic>;
final GeoPoint point = data['position']['geopoint'];
_addMarker(point.latitude, point.longitude);
});
}
}
まとめ
geoflutterfireについて少しは理解していただけたでしょうか! マップとFirebaseを使う際にはぜひ使用してみてください!
Discussion