はじめに
2024/6 (v0.23.0) に追加された Map コントロールが面白そうなので、使い勝手を確認してみる。
Map コントロールの情報
- ドキュメント: https://flet.dev/docs/controls/map/
- さまざまなレイヤーを含むマップを表示するために利用できるもの
前提事項
- Flet v0.24.1
調査内容
サンプルを実行してみる
まずは、ドキュメントにあるサンプルをコピペして実行してみる。
- 問題なく表示され、初期表示はアフリカ大陸
- クリックでマーカーを置くことができる
- 右クリックで〇を置くことができる
- マウスホイールで拡大縮小できる
- ドラッグで動かすことができる
- 日本の情報もそれなりに入っている
- 一番縮小すると変な感じになるので制御可能なのか気になった(地球4.7個分ぐらい見える)
- 地図のもとは OpenStreetMap というサービス … https://www.openstreetmap.org/about
サンプルソース
import random
import flet as ft
import flet.map as map
def main(page: ft.Page):
marker_layer_ref = ft.Ref[map.MarkerLayer]()
circle_layer_ref = ft.Ref[map.CircleLayer]()
def handle_tap(e: map.MapTapEvent):
print(
f"Name: {e.name} - coordinates: {e.coordinates} - Local: ({e.local_x}, {e.local_y}) - Global: ({e.global_x}, {e.global_y})"
)
if e.name == "tap":
marker_layer_ref.current.markers.append(
map.Marker(
content=ft.Icon(
ft.icons.LOCATION_ON, color=ft.cupertino_colors.DESTRUCTIVE_RED
),
coordinates=e.coordinates,
)
)
elif e.name == "secondary_tap":
circle_layer_ref.current.circles.append(
map.CircleMarker(
radius=random.randint(5, 10),
coordinates=e.coordinates,
color=ft.colors.random_color(),
border_color=ft.colors.random_color(),
border_stroke_width=4,
)
)
page.update()
def handle_event(e: map.MapEvent):
print(
f"{e.name} - Source: {e.source} - Center: {e.center} - Zoom: {e.zoom} - Rotation: {e.rotation}"
)
page.add(
ft.Text("Click anywhere to add a Marker, right-click to add a CircleMarker."),
map.Map(
expand=True,
configuration=map.MapConfiguration(
initial_center=map.MapLatitudeLongitude(15, 10),
initial_zoom=4.2,
interaction_configuration=map.MapInteractionConfiguration(
flags=map.MapInteractiveFlag.ALL
),
on_init=lambda e: print(f"Initialized Map"),
on_tap=handle_tap,
on_secondary_tap=handle_tap,
on_long_press=handle_tap,
on_event=handle_event,
),
layers=[
map.TileLayer(
url_template="https://tile.openstreetmap.org/{z}/{x}/{y}.png",
on_image_error=lambda e: print("TileLayer Error"),
),
map.RichAttribution(
attributions=[
map.TextSourceAttribution(
text="OpenStreetMap Contributors",
on_click=lambda e: e.page.launch_url(
"https://openstreetmap.org/copyright"
),
),
map.TextSourceAttribution(
text="Flet",
on_click=lambda e: e.page.launch_url("https://flet.dev"),
),
]
),
map.SimpleAttribution(
text="Flet",
alignment=ft.alignment.top_right,
on_click=lambda e: print("Clicked SimpleAttribution"),
),
map.MarkerLayer(
ref=marker_layer_ref,
markers=[
map.Marker(
content=ft.Icon(ft.icons.LOCATION_ON),
coordinates=map.MapLatitudeLongitude(30, 15),
),
map.Marker(
content=ft.Icon(ft.icons.LOCATION_ON),
coordinates=map.MapLatitudeLongitude(10, 10),
),
map.Marker(
content=ft.Icon(ft.icons.LOCATION_ON),
coordinates=map.MapLatitudeLongitude(25, 45),
),
],
),
map.CircleLayer(
ref=circle_layer_ref,
circles=[
map.CircleMarker(
radius=10,
coordinates=map.MapLatitudeLongitude(16, 24),
color=ft.colors.RED,
border_color=ft.colors.BLUE,
border_stroke_width=4,
),
],
),
map.PolygonLayer(
polygons=[
map.PolygonMarker(
label="Popular Touristic Area",
label_text_style=ft.TextStyle(
color=ft.colors.BLACK,
size=15,
weight=ft.FontWeight.BOLD,
),
color=ft.colors.with_opacity(0.3, ft.colors.BLUE),
coordinates=[
map.MapLatitudeLongitude(10, 10),
map.MapLatitudeLongitude(30, 15),
map.MapLatitudeLongitude(25, 45),
],
),
],
),
map.PolylineLayer(
polylines=[
map.PolylineMarker(
border_stroke_width=3,
border_color=ft.colors.RED,
gradient_colors=[ft.colors.BLACK, ft.colors.BLACK],
color=ft.colors.with_opacity(0.6, ft.colors.GREEN),
coordinates=[
map.MapLatitudeLongitude(10, 10),
map.MapLatitudeLongitude(30, 15),
map.MapLatitudeLongitude(25, 45),
],
),
],
),
],
),
)
ft.app(main)
初期表示位置を変えてみる
初期表示位置を東京駅に変えてみる。
- コントロールの定義に設定があるので変更した
- ★1 で緯度と経度を指定するとそこが中心になるので、東京駅の緯度経度を調べて設定
- ★2 は、zoomが 4.2 だと足りないのでちょうどよさそうな 16 に変更
↓変更箇所抜粋
map.Map(
expand=True,
configuration=map.MapConfiguration(
initial_center=map.MapLatitudeLongitude(35.6809591, 139.7673068), # ★1
initial_zoom=16, # ★2
interaction_configuration=map.MapInteractionConfiguration(
flags=map.MapInteractiveFlag.ALL
),
マーカーの表示を変更してみる
サンプルのマーカー表示がいまいち見えづらいので、見えやすい表示に改善する。
- Icon だけだと地図との境界がなくて見えづらい
- IconButton なども試したが、CircleAvatar に Icon を入れるが一番良さそうである
- 試しに食事場所をプロットするイメージのアイコンにしてみた
↓変更箇所抜粋
def handle_tap(e: map.MapTapEvent):
print(
f"Name: {e.name} - coordinates: {e.coordinates} - Local: ({e.local_x}, {e.local_y}) - Global: ({e.global_x}, {e.global_y})"
)
if e.name == "tap":
marker_layer_ref.current.markers.append(
map.Marker(
content=ft.CircleAvatar( # ここを変更
content=ft.Icon(ft.icons.DINING_OUTLINED),
color=ft.colors.RED,
bgcolor=ft.colors.WHITE,
),
coordinates=e.coordinates,
)
)
縮小しすぎるのを防ぐ
デフォルトで最大まで縮小すると小さくなり過ぎるので制御をいれる。
- これは min_zoom を設定するだけで縮小を止めることが可能
- 今回は、min_zoom=12 ぐらいがちょうど良かった
- 同様に拡大も行き過ぎるので、max_zoom=18 を設定した
- Map を使うときは min_zoom, max_zoom を指定した方が良さそう
↓変更箇所抜粋
map.Map(
expand=True,
configuration=map.MapConfiguration(
initial_center=map.MapLatitudeLongitude(35.6809591, 139.7673068), # ★1
initial_zoom=16, # ★2
interaction_configuration=map.MapInteractionConfiguration(
flags=map.MapInteractiveFlag.ALL,
),
min_zoom=12, # ここを追加
max_zoom=18, # ここを追加
情報が入力できるようにしてみる
マーカーを置くときに情報を入力するステップを追加する。
- マーカーを配置後、BottomSheet を表示
- 保存ボタンをクリックしたら情報を保存する(とりあえず保存はしない)
- キャンセルならマーカーを削除する
↓変更箇所抜粋(BottomSheet の実装)
def handle_bs_save(e):
# TODO: ここで情報を保存する
page.close(bs)
def handle_bs_cancel(e):
del marker_layer_ref.current.markers[-1] # 最後のマーカーを削除する
page.update()
page.close(bs)
def bs_open():
tf_comment.value = "" # コメントクリア
page.open(bs)
tf_comment = ft.TextField(
label="コメント", multiline=True, max_lines=5, autofocus=True
)
bs = ft.BottomSheet(
dismissible=False,
content=ft.Container(
padding=20,
content=ft.Column(
tight=True,
controls=[
ft.Text("情報"),
tf_comment,
ft.Row(
[
ft.ElevatedButton("保存", on_click=handle_bs_save),
ft.ElevatedButton("キャンセル", on_click=handle_bs_cancel),
]
),
],
),
),
)
↓変更箇所抜粋(tapでBottomSheetを表示)
def handle_tap(e: map.MapTapEvent):
・・・省略・・・
if e.name == "tap":
marker_layer_ref.current.markers.append(
map.Marker(
content=ft.CircleAvatar( # ここを変更
content=ft.Icon(ft.icons.DINING_OUTLINED),
color=ft.colors.RED,
bgcolor=ft.colors.WHITE,
),
coordinates=e.coordinates,
)
)
page.update()
bs_open() # BottomSheetを表示
マーカーをクリックしたら情報を表示する
配置したマーカーをクリックしたら情報を表示するようにする。
- マーカーをクリックできるように Container で囲い、クリックの処理を足す
- BottomSheet の表示を新規と編集で出し分ける
- 編集のときは、保存するか、単に閉じるだけにする
↓変更箇所抜粋(BottomSheet を編集の場合に対応)
def bs_open(edit=False):
tf_comment.value = "" # コメントクリア
buttons[1].visible = True
buttons[2].visible = False
if edit: # 編集の場合
tf_comment.value = "ダミーコメント" # TODO:保存してある情報を持ってくる
buttons[1].visible = False
buttons[2].visible = True
page.open(bs)
・・・省略・・・
buttons = [
ft.ElevatedButton("保存", on_click=handle_bs_save),
ft.ElevatedButton("キャンセル", on_click=handle_bs_cancel),
ft.ElevatedButton("閉じる", on_click=handle_bs_close),
]
bs = ft.BottomSheet(
dismissible=False,
content=ft.Container(
padding=20,
content=ft.Column(
tight=True,
controls=[
ft.Text("情報"),
tf_comment,
ft.Row(buttons),
],
),
),
)
↓変更箇所抜粋(Marker の設定を変更)
def handle_tap(e: map.MapTapEvent):
・・・省略・・・
if e.name == "tap":
marker_layer_ref.current.markers.append(
map.Marker(
content=ft.Container(
ft.CircleAvatar( # ここを変更
content=ft.Icon(ft.icons.DINING_OUTLINED),
color=ft.colors.RED,
bgcolor=ft.colors.WHITE,
),
on_click=lambda e: bs_open(True), # クリックでBottomSheetを開く
),
・・・省略・・・
まとめ
Map コントロールを使えば、地図上に情報をプロットすることが簡単にできることが確認できた。
マーカーにはコントロールが指定できるので、工夫すればいろいろなことができそうではある。
以上です。