Google Map Javascript API の script について

概要

Google Map Javascript API で地図を表示する際の script 要素について説明し、Typescript + Next.js 環境での実装を行った。

背景

Google Map Javascript API の introduction では、以下の魔法の script を貼るとよい、みたいな書かれ方をしている。 JavaScript を使用するマーカーが配置された Google マップを追加する  |  Maps JavaScript API  |  Google for Developers

<script>
  (g => {
    var h, a, k,
      p = "The Google Maps JavaScript API",
      c = "google",
      l = "importLibrary",
      q = "__ib__",
      m = document,
      b = window;
    b = b[c] || (b[c] = {});
    var d = b.maps || (b.maps = {}),
      r = new Set,
      e = new URLSearchParams,
      u = () => h || (h = new Promise(async (f, n) => {
        await (a = m.createElement("script"));
        e.set("libraries", [...r] + "");
        for (k in g) e.set(k.replace(/[A-Z]/g, t => "_" + t[0].toLowerCase()), g[k]);
        e.set("callback", c + ".maps." + q);
        a.src = `https://maps.${c}apis.com/maps/api/js?` + e;
        d[q] = f;
        a.onerror = () => h = n(Error(p + " could not load."));
        a.nonce = m.querySelector("script[nonce]")?.nonce || "";
        m.head.append(a);
      }));
    d[l]
      ? console.warn(p + " only loads once. Ignoring:", g)
      : d[l] = (f, ...n) => r.add(f) && u().then(() => d[l](f, ...n));
  })({
    key: "YOUR_API_KEY",
    v: "weekly"
  });
</script>

これを Typescript + Next.js の環境で使えるように変えたかった。その中で理解したことを書き記す。 もし、見当違いやもっと良い捉え方などあればぜひコメントしていただきたい。

Typescript + Next.js 版

以下のコードとした。GoogleMap コンポーネントの位置に google map が貼られる。

"use client";
import { useEffect, useRef, useState } from "react";
import Script from "next/script";

export default function GoogleMap() {
  const mapRef = useRef<HTMLDivElement>(null);
  const [isLoaded, setIsLoaded] = useState(false);

  useEffect(() => {
    if (window.google?.maps) {
      initializeMap();
      return;
    }
  }, []);

  // Google Map の初期化
  function initializeMap() {
    if (!mapRef.current || !window.google) return;

    window.google.maps
      .importLibrary("maps")
      .then((mapsLib) => {
        const { Map } = mapsLib as { Map: typeof google.maps.Map };
        const map = new Map(mapRef.current!, {
          center: { lat: 35.6895, lng: 139.6917 }, // 東京
          zoom: 10,
        });
        console.log("Google Map initialized!", map);
        setIsLoaded(true);
      })
      .catch((err) => {
        console.error("importLibrary error:", err);
      });
  }

  return (
    <>
      <div style={{ width: "100%", height: "500px", position: "relative" }}>
        <div ref={mapRef} style={{ width: "100%", height: "100%" }} />
        {!isLoaded && (
          <p
            style={{
              position: "absolute",
              top: 0,
              left: 0,
              backgroundColor: "#fff",
              padding: "8px",
            }}
          >
            Loading Map...
          </p>
        )}
      </div>
      <Script
        id="google-maps-script"
        src={`https://maps.googleapis.com/maps/api/js?key=${process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY}`}
        strategy="beforeInteractive"
        onLoad={initializeMap} // ロード後に `handleScriptLoad` を実行
      />
    </>
  );
}

解説 (元の script)

script タグ内には即時実行関数が書かれている。引数は g={key: "YOUR_API_KEY", v: "weekly"}。以下関数内の詳細を記す。

    b = b[c] || (b[c] = {});
    var d = b.maps || (b.maps = {}),

これは、windows.google.maps というグローバル変数がなければ作っている。 この windows.google.maps は script が読み込まれると定義されるものであり、これを用いて google map を操作できるようになる。

u = () => h || (h = new Promise(async (f, n) => { ... }

h はメモ化のための変数。Promise を何回も作らないようにしている。 u は結局 Promise オブジェクトで、f が resolve, n が reject。この関数を実行すると、head に script を定義し、Google Map Javascript API を読み込む。 以下関数 u についての詳細となる。

await (a = m.createElement("script"));

script 要素を作成。

e.set("libraries", [...r] + "");
for (k in g) e.set(k.replace(/[A-Z]/g, t => "_" + t[0].toLowerCase()), g[k]);
e.set("callback", c + ".maps." + q);
a.src = `https://maps.${c}apis.com/maps/api/js?` + e;
d[q] = f;

Google Map Javascript API を呼び出す URL のパラメータを定義している。 1 行目は追加のライブラリを設定しているが、これは昔の library 追加方法の遺産っぽい。 2 行目は即時関数の引数 g を parse している。key を lower camel case から snake case にして、URL のパラメータに入れている。 3 行目は Google Map Javascript API を読み込んだあとに google.maps.ib という関数を呼び出すようにしている。この google.maps.ib は 5 行目で定義されていて、resolve() を実行しているだけに過ぎない。 これによって、読み込み終了がわかる。 4 行目で script 要素に URL を設定。

a.onerror = () => h = n(Error(p + " could not load."));

失敗したときのエラーハンドリング

a.nonce = m.querySelector("script[nonce]")?.nonce || "";

セキュリティ関連らしい。

nonce - HTML: ハイパーテキストマークアップ言語 | MDN

m.head.append(a);

head に今まで設定した script を追加。

以上が関数 u の定義。

d[l] ? console.warn(p + " only loads once. Ignoring:", g)
      : d[l] = (f, ...n) => r.add(f) && u().then(() => d[l](f, ...n));

window.google.maps.importLibrary を仮定義する。すでに定義されていれば無視。 最近のバージョンでは、window.google.maps.importLibrary は関数 u が実行されれば Google Map Javascript API によって定義される。それまでの間に importLibrary を実行しても大丈夫なように定義している。 具体的には、u() が実行されたあともう一度実行されるようにしている。 その前のバージョンにも対応していて、Set r 二ライブラリ名を追加して、関数 u を実行することで追加するようにしている。

Next.js 版との違い

面倒な定義あたりを next/Script に押し付けられている。Script はリロードしたときに onLoad を実行しないので、useEffect が必要な点に注意。

連絡先交換用NFCカード作成

これはタイガーさんがやっていたことをパクらせてもらった

やりたいこと

NFCカードをかざすだけで LINE 交換やインスタ交換をしたい

それによるメリット

  • どこにQRコードあったっけと探す時間がなくなる。
  • 非IT系に対して、ITで便利になる感を出せる

やり方

NFC カードに自分のサイトの URL を仕込む

NFC カード

無地だと10枚で1000円以下で安い。 URL書き込みも無料アプリがあって、何度でも書き込める。

サイト作成

連絡先交換用なので、凝らなくてよく、かつ 1 ページ以内(無駄な操作がない)ようにしたい。 したがって、少ない労力でサイトが作れる Google サイトを用いた。

結果

TBW

ISUCON13参加記

当日

チーム MONOS で参加

github.com

開始前

朝早すぎる、9:40 なんか間に合わなかったが、開始時刻には間に合った

11時まで

ベンチがうまく回らない。計測結果が出なかった。とりあえず規則熟読、サイトを触ってみて、コードを読み始めていた。

午前中

NG word

計測が出ていなかったので、NG word 職人をしていた。NG word の判定をアプリ側で行うようにしたり、必要のない実装を消したりした。

ランキング

N+1 (select 関数 where hoge) を group by で解消

中盤戦

計測結果が出た。できるだけ上位のものに取り掛かる

予約機能

いらない Select を削除

fillUser の修正

これがやばかった。fill~という関数の依存関係がかなり密で、fillUser を呼んでいるところも多かった。全体の高速化は煩雑になるためあきらめ、特にボトルネックとなっていた、GET /api/livestream/:livestream_id/livecomment と GET /api/livestream/:livestream_id/reaction から、fillUser までの部分が早くなるように修正を行った。 この呼び出され方では livestream_id は一意であることをドメイン知識として入れ込むイメージでの実装。 ここで点数が倍の 3 万くらいまで伸び、瞬間 2 位になった。

終盤戦

search の修正

fill 関数のループ削除をしようとした。しかし、逆に点数が落ちた。noe くんの助言を受け、一部のみを適用したところ点数が少し上がった。

結果

終結果としては 11 万点ほど、700 人中 13 位。これまで 3 回ほど出てきたが、ようやくまともに戦えるようになったなと思う一方、キャッシュ化、nginx、インフラ周りといった知識のない部分が多すぎるため、これから学んでいきたい。

その他

saza くんは、ansible 含むインフラ周り、分散+今回一番難しい DNS 改善に挑んでいた。私にはよくわからない部分をやってくれていたので、感謝。 noe くんは、icon 含むデータのキャッシュ化、SQL, アプリ改善に挑んでいた。割と近めな部分+私が怪しい部分をやってくれており、ISUCONらしいワイワイ感を感じていた。感謝。

バックエンドアプリを AWS 上に乗せたい

AWS にアップロード

EC2 インスタンスの作成

最初なのでおすすめされた amazon linux にして、t2.micro にした。セキュリティは HTTP、HTTPS を任意、SSH を自分の IP のみとした。

Elastic IP を作成、グローバル IP を固定。1 つ目は無料っぽい。

SSH config をいじって ssh aws で通信できるようにした。

IAM ダッシュボードで MFA(多要素認証) すべきといわれたのでやる。連続する 2 つのコードは、30 秒前のコードと今のコード(更新前と後) の 2 つを入力する。

ec2 に Name=app1, Env=dev のタグをつける。 IAM ユーザー作成、アクセスキー作成、aws configure でアクセスキー、シークレットアクセスキー、リージョン設定。インラインポリシー追加より下記の JSON を追加。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ssm:PutParameter",
                "ssm:DeleteParameter",
                "ssm:GetParameterHistory",
                "ssm:GetParametersByPath",
                "ssm:GetParameters",
                "ssm:GetParameter",
                "ssm:DeleteParameters"
            ],
            "Resource": "arn:aws:ssm:(リージョン):(アカウント ID):parameter/app1/dev/*"
        }
    ]
}

これで ssh して aws ssm put-parameter などで Parameter Store が使えるようになった。

mysql インストールしようとしたら、glibc-2.28 の libc.so.6 がないと言われた、どうしようもなさそうなので、EC2 再構築、mariadb をインストールすることにした。 CURRENT_TIMESTAMP が使えない、バージョンが 5.5 のせい。 【Amazon Linux 2】MariaDBを5.5から10.5にバージョンアップ|IT石ログ これに沿ってアップデートすれば使えるようになった。

sudo vim /etc/environment環境変数を設定。port が 1024 以下の時は sudo が必要。root ユーザーでも aws configure の設定する。

エラー: address already in use 、前に動かしたものが残っている。 ps ax | grep go からプロセスを見て、kill する。

gmail 送信でエラー(認証系)が出る。安全性低いアプリを許可していたが、これを機にやめてみる。二段階認証、アプリパスワード生成して解決。

以上により AWS に上げることができた。

nohup で常に起動ができる。

次は AWS VPC とか使ってみたい。

タグ付けアプリについて

タグ付けする Web サイト(ブックマーク機能)を製作している。そのサイトのバックエンドを担当した。

GitHub - moririn2528/tagtagyeah: 文献にタグをつけ、検索する web アプリを作る

 

概要

何かしらの Web サイトを作りたいと思い、友人にどのようなサイトが欲しいか聞いたところ、タグ付けサイトが上がったため製作することとなった。このサイトのフロントエンドは別の友人が製作している。

基本的な機能は、ユーザー登録、ログインして、文献、URL にタグをつける、そして、タグを与えると、そのタグが付いた文献を表示する、というものである。

 

以下話すことがある部分について書く。

検索機能について

タグの検索機能を付けた。

最初は prefix が一致するものをすべて返す、という実装。

これだと 1 文字間違えると候補として出ない。また、真ん中の単語だけを覚えていたとしても検索できない。

これらの対処法として、2 グラムを用いた。また、検索単語、タグ名の 2 グラムで一致するものの数を類似度とし、類似度が大きいものから順に出力することにした。

用いた理由
  • 文字の並びの情報をある程度保持している
  • 検索単語とタグ名が同じの時、類似度は文字列の長さを n とすると n-1 になる。そして、類似度 n-1 のタグは、文字列が小さいとき文字列内で同じ文字が使われることは少ないので、多くの場合で検索単語と同じといえる。
  • 編集距離 1 (1 文字削除、追加、置換) による類似度の減少は高々 2 であるので、類似度が高いものは検索単語に近いと示される
  • タグ名、検索文字列は数文字から十数文字と考えられる。よって、3 グラム以上にすると検索効率が落ちると思われる (例えば、検索単語 5 文字で真ん中の単語が違うとき検索結果は意味がなくなるため)

 

今回、データベースから検索するユーザーが登録したタグすべてを取ってきて、それぞれのタグに対し類似度を調べ、ソートしている。計算量は検索単語文字列長 m、タグ名の平均文字列長 n、タグ数 t とすると O(nmt)

 

タグ名をランダムなアルファベット 20 文字とし、10 万個のタグを登録した上で検索する。

検索結果

3 文字以上の連続部分文字列であれば、上位 3 つのうちに正しいタグが出る。また、5 文字の連続部分文字列の 3 文字目を置換して検索しても上位 3 つのうちに正しいタグが出る。

これらから、検索機能はうまくいっているといえる。

 

計算時間

検索文字列が 5 文字のとき、データベースからタグ名取得、上記アルゴリズム、それ以外の処理、それぞれの時間を計測し平均すると、0 ms, 365 ms, 1 ms になった。

高速化を行う。

検索文字列の 2 グラムを map に保存する、これで計算量は O(nt) となる。実際、時間を計測すると 110 ms になった。

タグ名の 2 グラムをテーブルに保存すると、アルゴリズムの計算時間はさらに小さくなるが、データの大きさが増える上、取ってくるときの時間がさらにかかる。データベースの無料枠が小さいのも相まって、この方法は取らないことにした。

 

ログイン機能について

ログイン機能を付けた。ID、パスワード、メールアドレスを登録すると、そのメールアドレスに認証メールが届く。その後、認証メールに記載されたリンクに行くと認証されて、ログインすることが可能となる。

ログインすると、一時的な ID を発行、その ID で他の関数を使うことが可能となる。

 

こういう機能を付けるのが初めてだったので、どういう構造にするべきかかなり悩んだ。JWT 認証とかも考えたが、結局使わなかった。

 

まとめ

これらによって、Go 言語、MySQL に慣れ、バックエンドに触れることができた。しかし、このアプリはまだ多くのユーザーに使われていない、同期処理などもしておらず、実際にたくさんの人に使われたらどうなるのかわからないという問題点がある。

xor convolution

数列 A から数列 B を以下で生成

 b_k = \sum_{i \oplus j = k} a_i a_j
( \oplus は bitwise xor とする)

 

まず、数列 A を多項式

 f(x) = a_0 + a_1 x^1 + \dots + a_n x^n

 と表す。 x^i \times x^j = x^{i \oplus j} となれば、FFT を用いて  O(n \log n) で計算可能。

ここで、 bit(i,j) i j ビット目とし、

 x^i = \prod_j x_j^{bit(i,j)}

とする。例を挙げると、 x^3 = x_0^1 x_1^1 x^5 = x_0^1 x_1^1 x_2^1 となる。これは、xor がビットごとの計算である性質を表している。

次に、 x_k^i \times x_k^j = x_k^{i \oplus j} x_k^i \times x_k^j = x_k^{(i+j) mod 2} は同値である。 x_k^2 = 1、つまり  x_k = -1, 1 のとき、 \times を乗算とすると上記の性質を持つようになり、  x^i \times x^j = x^{i \oplus j} となる。これによって計算可能。実装は高速メビウス、ゼータ変換に似た形で可能。

 

参考

説明

Fast Walsh Hadamard Transforms and it's inner workings - Codeforces

実装

色々な畳み込み - kazuma8128’s blog

HAL研究所プログラミングコンテスト2020

HAL研究所プログラミングコンテスト2020 に参加した。課題や競プロなどによって、3 日ほどしかできなかった。

 

実装

地形の境界に注目した。境界で、かつ格子点上の点と、巻物、最初のウサギの位置の点すべてを頂点とし、そのなかの任意の 2 つの頂点間の距離を、地形に応じた距離(砂地なら 10/3 倍、池なら 10 倍にする)として計算する。これは、通過するマスを 1 つずつ見ていって計算した。

その後、巻物の点、最初のウサギの位置の点からダイクストラを回し、巻物の点と最初のウサギの位置の点を頂点とする完全グラフを作成する。2 点の距離は、ダイクストラによって得られたパスに沿って実際にウサギが飛んで行った時の飛ぶ回数とした。これは、二分探索を改良した形で実装した。

この完全グラフを得た後、巡回セールスマン問題を解いた。(2^N)*(N^2) かかる方法で実装した。

そして、得られたパスに沿って進む、という風に書いた。

 

高速化

実装にかなりの時間がかかったうえ、実装すると全体で 140 秒かかってしまい、制限時間 6 0 秒を超えてしまった。二分探索の部分を高速化することで 100 秒まで落としたが、この時のボトルネックとなった、巡回セールスマン問題の部分と地形に応じた距離を調べる部分を高速化することができなかった。

 

やりたかったこと

二分探索みたいにすることで、よくなる時と悪くなる時があった。悪くなる時は、平地から少し先の池の部分に飛んでしまうようなパターンであった。この時に平地のぎりぎりで止まる、みたいなことをしたかった。

また、巻物を境界付近に持ってくる(巻物がある地点を踏んだらすぐ次に行く)実装もしたかった。

 

反省点

実装力が足りない。

また、定数倍高速化を今まであまりしてこなかったので、それほどできなかった。