29
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

学園祭プログラマーAdvent Calendar 2019

Day 18

遭難者を救うために学内Mapを作った話

Last updated at Posted at 2019-12-18

自己紹介

こんにちは!2019年度の筑波大学園祭実行委員でWeb担当をしていました @SIY1121 です。
今回は学園祭の来場者向けWebマップアプリを開発しました!
すべて紹介するとかなり量が多くなるので。地図の表示部分に絞って少し紹介したいと思います。

ざっくりいうと

  • 全国2位の広さを誇る筑波大学 筑波キャンパス で学園祭が行われた
  • 広いだけに、来場者が迷ってしまう
  • Vue.js + SVG + 自作webpackloader でGoogleMapsライクなWebアプリを開発した
  • コア機能はOSS化予定!

so.png

スマホに最適化されています↓
https://www.sohosai.com/map

遭難者が相次ぐ大学

筑波大学 筑波キャンパスは全国で2番目に広いキャンパスです。
その大きさは258ha(ディズニーランド5個分)にもなります。

雙峰祭

雙峰祭(そうほうさい)は、毎年3万人以上が訪れる筑波大学の学園祭です。
一般の方がたくさんやってきますが、広いので迷う方も多いです。

Webマップを作った

迷ってしまってもスマホで現在位置を調べたり、企画を調べたりできるWebアプリを作りました。
期間中10万ページビューを達成し、GPSを使用したユーザーは1.5万人にも登りました。
so.png

使ったもの

Vue.js

言わずと知れたフロントエンドのフレームワーク。

SVG

地図はイラレで作成し、SVGで出力しています。
SVGを使用することにより、拡縮しても画質が劣化しない上、建物や文字、アイコン等の要素を個別に制御することができるので

  • 建物がタップできる
  • 地図をピンチイン・アウトしても文字やアイコンのサイズや角度を固定したりできる

といったマップに欠かせない機能が実装しやすくなります。

図2.png

eazy-pz-as

マップを拡縮したり回転できる機能は、easy-pz-asを使用しました。
指定したsvgを拡縮したり移動したりできるようになります。
ただ、これはVue.jsを想定した作りになっていないので、使うには多少工夫が必要でした。

自作webpack loader

以下の3つの理由からwebpack loader を自作しました。

  • Vue.js でSVGを制御するには .vue ファイルにSVGをインラインで記述する必要があり、イラレで生成された数万行のSVGを vueファイルに入れるのは扱いにくい
  • 文字やアイコンのサイズや回転を固定するには、一つ一つの要素に変形の中心座標を設定する必要があるため、すべての要素の座標を調べ、SVG内にベタ書きするのは人間がすることではない
  • デザインの変更などにより、SVGがバージョンアップされるため、上記のようにSVGに変更を加えていると、もう一度変更し直す必要がある

そもそもwebpack loader とは?

webフロントエンド向けアセットバンドラ「webpack」の内部でファイルを変換したり、依存関係を構築するする工程で使用されます。
身近な例では、Vue.js単一コンポーネントや、Reactのjsxなど、ブラウザがそのままでは理解できないファイルをjsに変換するのに使われています。

自作webpack loader で何をしてるか

so.png

今回作成したwebpack loaderは SVGにVueで制御するために必要な属性を自動で付与し、.vue にインライン展開します。
具体的には、指定した要素(アイコンや文字)のサイズや回転を固定するためにスタイルをバインディングします。

一番簡単なwebpack loader

作り方はとっても簡単で、ソースコードをstringで受け取り、何かしらの処理を施したソースコードを返す関数をexportするだけです。

map-loader.js
// webpackはnode.js上で動くため、CommonJS
module.exports = function(source) {
  // 処理
  return source
}

どのファイルにどのwebpack loaderを使うかはwebpackのconfigで定義できます。
VueやReactで専用のツールチェインを使う場合でも、それぞれの方法でwebpackのconfigを設定できます。

今回はこの処理の中で SVG の処理&挿入を行っています。

SVGに属性を付加する

Vueファイルには <img src="/path/to/svg" svg-map > のように書いておき、正規表現で挿入するsvgファイル名を抽出します。(jsdomを使ってもいいかも)
次に、抽出したパス名からSVGファイルを読み込みます。読み込んだsvgはただの文字列なのでjsdomを使ってDOMを生成し、扱いやすくします。

map-loader.js
let svgString = fs.readFileSync(filePath, 'UTF-8')

const document = new JSDOM(svgString).window.document
const elements = document.querySelectorAll('#アイコン > g')
elements.forEach((el, index) => {
  const refID = `icon-${index}`
  el.setAttribute('ref', refID)
  el.setAttribute(':style',`getElementStyle('${refID}')`)
})

svgString = root.outerHTML // 属性が付与されたsvg文字列

上の例は、svgを読み込み #アイコン直下の要素(イラレ上でアイコンレイヤー直下のオブジェクト)に対して ref:style属性を付与しています。
最後に、先程の imgタグを処理後のsvgStringで置換して、sourceとして返却します。

挿入先のVueファイル

挿入先のVueファイルに以下のようなメソッドを追加しておきます。
webpack loader で :styleにバインドしたgetElementStyleがこれに相当します。

Map.vue
getElementStyle(refID) {
  const target = this.$refs[el]
  if (!target) return ''

  /* 地図の変形に応じて要素も変形
    rotateZで要素を逆回転させると回転が固定されているように見える
    scaleで地図の拡大率の逆数を掛けることでサイズが固定されているように見える
  */
  const transform = `
        rotateZ(${this.mapTransform.rotate.deg * -1}deg)
         scale(${1 / this.mapTransform.scale})
        `

  // 要素を囲う最小の長方形を取得
  const box = target.getBBox()
  // 要素が変形する中心を指定
  transformOrigin = `${box.x + box.width / 2}px ${box.y + box.height / 2}px`

  // 要素に適用するstyleを返す
  return {
    transform,
    transformOrigin
  }
}

これで アイコンが地図の拡縮、回転を行っても固定されるようになります。
コンパイル時にvueファイルとSVGファイルが内部で統合されるので、巨大なvueファイルができることもなく、気軽にSVGを差し替えることもできます。

おわりに

初めてwebpack loaderを自作しました。
今回の知見を生かして、コア機能はOSS化を予定しています。
誰でも簡単にWebマップを作れるようになって、遭難者を救っていただければ幸いです。

29
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?