Mapbox Style API を使って画像を登録する

この記事は、[YAMAPエンジニアAdvent Calendar 2022](https://qiita.com/advent-calendar/2022/yamap-engineers) の12月25日の記事です。

# はじめに

YAMAPにはMapboxを使って地図を表示する機能があり、地図上にサービス独自のアイコンを表示するためにはアイコン画像をMapbox Styleに登録する必要があります。

アイコン画像はMapboxのダッシュボードを使うと登録できますが、アイコン画像のオリジンは我々のサービス内で管理されているので二重管理はしたくありません。

そこでサービス内で管理されているアイコン画像を [Mapbox Style API](https://docs.mapbox.com/api/maps/styles/) を使用してMapboxと同期することを考えます。

YAMAPのバックエンドシステムはRubyで実装されているため [mapbox-sdk-rb](https://github.com/mapbox/mapbox-sdk-rb) を使うことを第一に検討しましたが現時点で Style API に対応しておらず、プルリクエストを作ることを検討しましたがこれもコードベースを確認したところ簡単ではなさそうであるとわかったので、Mapboxライブラリを自ら作ることとしました。

# Mapboxkit

作成した gem は Mapboxkit です。

- https://github.com/yamap-inc/mapboxkit-rb

現時点では主に Mapbox Style API のすべてをサポートしています。

# 使い方

MAPBOX_ACCESS_TOKEN 環境変数にアクセストークンが設定されていれば、とくに gem の設定など必要ありません。

```rb:example.rb
# スタイル一覧を取得する
Mapboxkit.styles('__your_username')
```

明示的にアクセストークンを設定したい場合は configure メソッドを使用できます。

```rb:configure.rb
Mapboxkit.configure do |config|
  config.access_token = '__YOUR_ACCESS_TOKEN__'
end
```

もし一つのアプリケーションで複数のアクセストークンを扱いたい場合はクライアントインスタンスを直接作ることで解決できます。

```rb:create_multi_clients.rb
client1 = Mapboxkit::Client.new(access_token: '__READONLY_ACCESS_TOKEN__')
client2 = Mapboxkit::Client.new(access_token: '__READWRITE_ACCESS_TOKEN__')

client1.styles('__your_username')
client2.create_style('__your_username', payload)
```

# Mapbox Style に画像を登録する

YAMAPでは管理画面から100種類程度のアイコン画像を一つのzipファイルに圧縮し一度にアップロードすることができます。

そこでこのアップロードをトリガーにしてMapboxのスタイルにも同期します。

以下は実装は架空のものでYAMAPのそれとは大きく違いますが、おおむねこのような感じで実装できるはずです。

```rb:sync_images_to_sprite_on_mapbox.rb
# Table name: `map_icons`
#
# ### Columns
#
# Name              | Type               | Attributes
# ----------------- | ------------------ | ---------------------------
# **`id`**          | `bigint`           | `not null, primary key`
# **`name`**        | `string`           | `not null`
# **`created_at`**  | `datetime`         | `not null`
# **`updated_at`**  | `datetime`         | `not null`
class MapIcon < ApplicationRecord
  has_one_attached :svg # Mapbox Style のアイコン画像は SVG形式です
end

class MapIconsForm
  def initialize(zip_file:)
    @zip_file = zip_file
  end

  def save
    map_icons = create_or_update_from(@zip_file)

    SyncAllMapIconsJob.perform_later

    map_icons
  end

  def create_or_update_from(zip_file)
    # zip_file を展開して MapIcon を作成・更新する
    # (省略)
  end
end

class SyncAllMapIconsJob < ApplicationJob
  USERNAME = '__USERNAME__'
  STYLE_ID = '__STYLE_ID__'

  def self.call = new.call

  def call
    # Mapbox Style の画像登録APIでは一度に登録できる画像は25件に制限されている
    MapIcon.in_batches(of: 25) do |map_icons|
      upload_map_icons(map_icons.to_a)
    end

    # 画像を反映するために Mapbox Style の Sprite ID を更新する
    refresh_sprite
  end

  private

  # アイコン画像をアップロードする
  #
  # @param map_icons Array<MapIcon> 
  # @param payloads  Array<Hash<io: IO, filename: String>> 
  def upload_map_icons(map_icons, payload = [])
    map_icon = map_icons.shift

    unless map_icon
      return Mapboxkit.add_images_to_sprite(USERNAME, STYLE_ID, payload)
    end

    map_icon.svg.open do |file|
      payload << { io: file, filename: map_icon.name }

      upload_map_icons(map_icons, payload)
    end
  end

  def refresh_sprite
    # 以下のドキュメントの指示に従い、最新のstyleデータを取得し modified created フィールドを削除する
    # see: https://docs.mapbox.com/api/maps/styles/#update-a-style
    base_params = Mapboxkit.style(USERNAME, STYLE_ID)
      .except('modified', 'created')

    # sprite ID を更新する
    style_params = .merge(sprite: "mapbox://styles/#{USERNAME}/#{STYLE_ID}/{sprite_id}")

    Mapboxkit.update_style(USERNAME, STYLE_ID, style_params)
  end
end

map_icons_form = MapIconsForm.new(zip_file:)

map_icons_form.save
```


# はまったこと

### 新しい画像がスプライト画像に反映されない

画像をMapbox Styleに登録に成功するが、本番環境に全く反映されないということがありました。

調査したところどうやら Mapbox Style の Sprite ID が画像を登録しても一切更新されておらず、これでは古い画像を参照し続けるようでした。

そこで上の疑似コードの以下の部分をあとから追加し呼び出すようにしたところ、Sprite ID が更新され十数分ほど待つことでキャッシュが更新され本番環境に画像が反映されたことを確認できました。

```ruby
  def refresh_sprite
    # 以下のドキュメントの指示に従い、最新のstyleデータを取得し modified created フィールドを削除する
    # see: https://docs.mapbox.com/api/maps/styles/#update-a-style
    base_params = Mapboxkit.style(USERNAME, STYLE_ID)
      .except('modified', 'created')

    # sprite ID を更新する
    style_params = .merge(sprite: "mapbox://styles/#{USERNAME}/#{STYLE_ID}/{sprite_id}")

    Mapboxkit.update_style(USERNAME, STYLE_ID, style_params)
  end
```

# まとめ

- Mapbox Style を API で操作するために gem を作成しました。
- 作成した gem を使い Mapbox Style への画像の同期処理を自動化しました。

# さいごに

コードを正しく共通化や抽象化する能力はプログラマにとって必要なスキルでしょう。

メイン関数に書いていたコードの断片をループに置き換える、関数に切り出す、クラスやモジュールとして実装する。

日常的に行われるこれらの営みのその先にライブラリとしてプロジェクトの外に切り離していくというプロセスが続いているのだと思います。

より良いソフトウェアを作れるよう引き続き精進します。

おわり。