やなぎにっき

学んだことの記録

OCRとLeafletでイベント会場の目的地が一目で分かるマップを作った

この記事は、「「GMOペパボ エンジニア Advent Calendar 2024」の13日目の記事です🎄

私が所属しているminneが主催するハンドメイドイベント「minneのハンドメイドマーケット2024(通称ハンマケ)」のLPサイトに出展者のブースの位置が分かるマップ機能を作りました。

私はハンマケの他にも、過去にデザフェスや同人イベントのようなたくさんの出展者ブースが並んでいるイベントに行ったことがあるのですが、お目当てのブースをに行こうとして迷った経験が何度もあります。イベントは購入品(紙は重い)を背負い、固くて疲れやすく広い会場の床を人ごみの中歩きまわるので、なんとかスムーズにお目当てのブースに行きたいものです。

そんな悩みを解決するような機能がこのマップ機能です。

ハンマケのLPサイトでは出展者の個別ページに表示しました。

マップ機能を作った流れ

  • イデアの発端はpplogさん
  • senさんが地図を作る
  • miyakeyさんにOCR情報を出力する
  • yanagiがOCR情報を元にピンの配置などの地図を実装
  • きでりんさんにスタイルをあてる

という豪華メンバーによる素晴らしい連携プレイでリリースされました。

今回は地図の実装について書きます。

実装

OCR出力については今回割愛します。

PythonのライブラリやGoogle Cloud Vision APIなどで実現できそうです。miyakeyさんの出力コードを見る限り、誤検出を防いだりちょうどいい位置に調整するのにコツが必要そうでした。

地図の実装

利用したライブラリ

  • React
  • Leaflet
  • Leaflet React

サンプルコード

import leaflet, { CRS } from 'leaflet'
import 'leaflet/dist/leaflet.css'
import { ImageOverlay, MapContainer, Marker, Rectangle, Tooltip } from 'react-leaflet'
leaflet.Icon.Default.imagePath = 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/'

const Map = () => {
  // マップ画像のサイズ
  const imgWidth = 5591
  const imgHeight = 3237
  // 地図の範囲
  const bounds = new leaflet.LatLngBounds([0, 0], [imgHeight, imgWidth])
  // ブースのx,y座標
  const markerPosition = [2188, 1736]
  // 矩形の位置
  const rectangle = new leaflet.LatLngBounds(
    [markerPosition[0], markerPosition[1]],
    [imgHeight, imgWidth]
  )

  return (
    <div>
      {/* 地図 */}
      <MapContainer
        center={bounds.getCenter()}
        zoom={-1} // デフォルトの縮尺
        minZoom={-5} // 最小縮尺
        crs={CRS.Simple} // 緯度経度をシンプルなxy座標にする
        maxBounds={[
          [0, 0],
          [imgHeight, imgWidth]
        ]} // 地図の範囲
      >
        {/* 画像をオーバーレイ */}
        <ImageOverlay
          url={'http://example.com/map.jpg'} // マップ画像のURL
          bounds={bounds}
        />
        {/* マーカー */}
        <Marker position={[markerPosition[0] + 20, markerPosition[1]]} />
        {/* 矩形 */}
        <Rectangle bounds={rectangle} pathOptions={{ color: 'red' }} />
        {/* ツールチップ */}
        <Rectangle bounds={rectangle} pathOptions={{ color: 'black' }}>
          <Tooltip direction="bottom" offset={[0, 20]} opacity={1} permanent>
            ブース番号
          </Tooltip>
        </Rectangle>
      </MapContainer>
    </div>
  )
}

export default Map

Leafletという地図のライブラリを使います。

元々は地図を表示するライブラリなんですが、画像をオーバーレイ+座標系をxyにすることでやりたいことが実現できました。

画像をオーバーレイはreact-leafletだとmageOverlayで行います。

{/* 地図 */}
<MapContainer
  center={bounds.getCenter()}
  zoom={-1} // デフォルトの縮尺
  minZoom={-5} // 最小縮尺
  crs={CRS.Simple} // 緯度経度をシンプルなxy座標にする
  maxBounds={[
    [0, 0],
    [imgHeight, imgWidth]
  ]} // 地図の範囲
>
  {/* 画像をオーバーレイ */}
  <ImageOverlay
    url={'http://example.com/map.jpg'} // マップ画像のURL
    bounds={bounds}
  />

ピンを置く

ピンや範囲を指定した図、Tooltipなどを好きに置くことができます。

{/* マーカー */}
<Marker position={[markerPosition[0] + 20, markerPosition[1]]} />
{/* 矩形 */}
<Rectangle bounds={rectangle} pathOptions={{ color: 'red' }} />
{/* ツールチップ */}
<Rectangle bounds={rectangle} pathOptions={{ color: 'black' }}>
  <Tooltip direction="bottom" offset={[0, 20]} opacity={1} permanent>
    ブース番号
  </Tooltip>

補足

今回正確にはNext.jsで実装したのですが、react-leafletはwindowオブジェクトを使用するため、ssr: falseを指定してdynamic importする必要がありました。

感想など

実装してみた感想

最初はJSとCSSで1から実装しようとも考えたんですが、Leafletで簡単にGoogle Mapライクのように好きに動かしたり拡大が実装できました。イベントでは基本スマホで利用するので見やすさ・動かしやすさは大事ですね。

当日私はイベントスタッフとしてインフォブースにいたんですが、あるブースを探している来場者にこのページの地図を使って説明できたのがよかったです。自分が実装した機能をユーザーに使ってもらう様子に立ち会えるのは開発者として本当に嬉しい体験でした!

フォローしているユーザーマップ

これは自分が欲しいと言って(半ば勝手に)作ったのですが、出展者ページだけではなく、フォローしているユーザーを一つのマップで見れるようにもしてみました。

minneは作家のお気に入り機能があり、また出展者情報とminneのアカウントで紐づいているので実現できました。

他のイベントでも使いたい

ハンマケは全て通番なので番号だけで一意に識別ができるんですが、コミケコミティア、デザフェスを見た感じA001、B001……のように英字+1から採番で、かつ地図には数字しか表示されないのでOCRで数値を抽出できてもブースを一意に識別できないことに気づきました。

引用: https://designfesta.com/about-map/

画像にA001のように数字の前に英字をつけるなにがしを実装する→今回と同じようにOCR出力をするみたいにやるといいのかな。

参考

React-Leafletで地図の代わりに画像を使う - write ahead log