Google Apps ScriptでSVGを使ったイメージマップを操作 – 応用編【GAS】

前回の記事にて、SVG画像を利用した座席表アプリの基本構造を作りました。しかし、実用するにはまだ全然機能が装備されていません。そこで、今回はこれを最低限利用できるように色々と装備をします。

今回装備する内容は、座席のキープ、座席の情報、固定席の場合の処理、座席のリリース、毎日24時に自動でクリアの5点他、不都合が出ないように色々とエラートラップを追加します。

Google Apps ScriptでSVGを使ったイメージマップを操作 - 基礎編【GAS】

今回使用するファイルやツール

今回は前回の素材を更に拡張し、多くの席を用意し、割と自由に配置しています。また、SVGデータに対しても今回は更に手を加えています。また、UIを整える為に、Vue.jsおよびVuetifyも導入しています。

PC基準で作っていますが、Vuetifyの利用とSVGの自動スケールによって、またGAS側でもレンダリングにてレスポンシブ対応コードを入れています。

図:こんな感じのUIを構築します。

事前準備

実務で使う為に前回のSVG画像データおよびスプレッドシートに手を加えています。

SVGデータの改造

今回のSVG画像は机の部分が画像ファイルとして取り込んでる為、前半部分がテキスト化された画像データになっています。大分したのほうに、座席に関するドローのデータがあります。

改造した部分は

  • 全てのaタグのonClickは、以下の関数にし、gタグ直下のIDを取得するようにしています。
    seatinfo($(this).children().attr('id'))
    seatA1といったようなIDを引数として取り、seatinfo関数に渡すようになっています。
  •  タイルの縁取りであるstrokeの色を青に変更し、styleとしてstroke-width:3を設定。青い縁取りを追加しました。
  • textのサイズを小さめにして、ユーザ名が入り切るサイズに調整しました。
  • 初期の座席の値は空にしました。

以下が完成形の1座席分のデータです。

<a href="javascript:void(0);" onclick="seatinfo($(this).children().attr('id'));" id="a23">
    <g id="seatA2" transform="matrix(0.84466019,0,0,0.88518024,17.616658,98.837363)">
        <rect 
            style="stroke-width:3" 
            id="A2" width="94.380722" 
            height="69.397591" 
            x="24.057831" 
            y="45.33976" 
            class="bar"
            fill="#afdde9" 
            stroke="blue" />
        <text xml:space="preserve"
            style="font-size:16.31940592px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';fill:#1a1a1a;stroke-width:0.0867171"
            x="26.065416" 
            y="89.968636" 
            id="textA2" 
            transform="scale(0.99324444,1.0068015)">
        <tspan sodipodi:role="line" 
            id="spanA2" 
            style="fill:#1a1a1a;stroke-width:0.0867171;font-size:16.31940592px"
            x="26.065416" 
            y="89.968636"> </tspan>
        </text>
    </g>
</a>

inkscapeで修正

前述のようなブロックを一個用意してる状態で、あとはそのブロックを以下のような形で処理していくのがXMLを意識せずに楽にレイアウトを作成する事が可能です。

  1. 必要な数だけそのブロックをコピーして配置する
  2. メニューより編集→XMLエディタを開く
  3. 1.のブロックを触るとXMLエディタにその箇所にジャンプする
  4. ▶をクリックすると、ブロックの中身が出てくる。
  5. svg:gがブロックの外側(厳密にはそれをくくってるaタグのseatinfoの関数が入ってる箇所が一番外側)をクリックし、下のパネルのidを「seatA001」といった形で書き換える
  6. さらにその中のsvg:rectをクリックし、同じく下のパネルのidをA001と書き換える
  7. さらにさらにsvg:textのさらに下に属してるsvg:tspanをクリックし、パネルのidをspanA001とでも書き換える
  8. この作業を必要なブロック分だけidが重複しない形で書き替えていく。

VSCodeで作業するよりもこちらのほうが、IDを書き換える作業は非常に楽です。

図:SVGデータをinkscapeで書き換える様子

シートデータの準備

2つのシートのみとし、シートデータは前回同様ですが、増やした座席の分だけデータを増やしています。また、今回より

  • 固定フラグにチェックが入ってる場合は、表示した場合灰色の席となり、キープできなくなるよう制限を加えるになります。
  • ユーザリストからの表示名とIDを入れるようになります。入ってる場合キープ済みとなります。

また、トリガーにより毎日24時にこれらのデータのうち、固定フラグがオンになっていないものについてはデータが自動でクリアされるようになり、翌日はまたキープできるようになるので、リリース忘れしても問題ありません。

セットアップ

必ず利用する場合は以下の2つを実行して、整備します。スプレッドシートを開きメニューにある「座席表Plus」をクリックし

  • セットアップ :スプレッドシートのIDをプロパティに格納します。
  • リセットトリガー設置 :毎日24時にシートデータの固定席以外の座席登録をリセットする関数を実行するトリガーを設置します。

上記のセットアップが完了したら、以下の手順でウェブアプリケーションとして公開する必要があります。

  1. スクリプトエディタを開く
  2. 右上のデプロイをクリックし、デプロイを管理をクリック
  3. 左上からウェブアプリを選び、ユーザ実行権限とアクセスできるユーザを指定
  4. デプロイをクリックして、ウェブアプリのURLを取得する

図:デプロイして完了

ソースコード

HTML側コード

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>

    <!-- Vuetify関係 -->
    <link href="https://fonts.googleapis.com/css2?family=M+PLUS+1p:wght@400;700&display=swap" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/MaterialDesign-Webfont/7.0.96/css/materialdesignicons.min.css" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">

    <style>
      svg {
        width: 100%;
        height:300px;
      }

      svg a path{
        transition: fill 0.3s linear;
      }

      .bar {
        pointer-events: all;
      }

      .bar:hover { 
        opacity: 0.7;
        fill: red;
      }

      #map{
        width:100%;
        height:1px;
      }
    </style>

    <style>
			.fixed-bar {
				position: sticky;
				position: -webkit-sticky; /* for Safari */
				top: 0em;
				z-index: 9;
			}
			html::-webkit-scrollbar{  /* メイン画面だけスクロールバーを消す */
				display: none;
			}

			html, body{				  /* メイン画面のスクロールを抑止 */
				overflow: hidden;
			}

			::-webkit-scrollbar{
				width: 10px;
			}
			::-webkit-scrollbar-track{
				background: #fff;
				border: none;
				border-radius: 10px;
				box-shadow: inset 0 0 2px rgb(54, 56, 53); 
			}
			::-webkit-scrollbar-thumb{
				background: rgb(65, 44, 255);
				border-radius: 10px;
				box-shadow: none;
			}

			.v-toolbar__extension{
				font-weight:bold;
				text-shadow:1px 1px 1px #000, -1px 1px 1px #000, 1px -1px 1px #000, -1px -1px 1px #000;
			}

			.titlehead{
				font-weight:bold;
				text-shadow:1px 1px 1px #000, -1px 1px 1px #000, 1px -1px 1px #000, -1px -1px 1px #000;
			}

			.tomato{
				text-shadow: 0 0 10px blue;
			}
		</style>

    <script>
      //グローバル変数
      var seatman = "";

      //リサイズ時にウィンドウにフィットさせる
			window.onresize = function(){
				setGridHeight();
			}
			  
			//gridのサイズを自動でウィンドウにフィットする
			$(document).ready(function () {
				setGridHeight();
			});

      //座席表の高さを自動補正する関数
			function setGridHeight() {
				var layoutHeight = $(window).height() - 25;
				$('#svg5').css('height', layoutHeight + 'px');
        $('#gridarea').css('height', layoutHeight + 'px');
			}

      //SVGファイルデータを呼び出し挿入する
      function init(){
        google.script.run.withSuccessHandler(onSuccess).svgload();
      }

      function onSuccess(data){
        //SVG画像を挿入する
        $("#map").html(data);
        setGridHeight();

        //シートデータを取得する
        google.script.run.withSuccessHandler(onSuccess2).seatload();
      }

      //シートデータを配列に格納する
      function onSuccess2(data){
        //データを取得する
        seatman = JSON.parse(data);

        //シートデータを反映する
        for(var i = 0;i<seatman.length;i++){
          //レコードを一個取り出す
          let rec = seatman[i];

          //割当ユーザ名を取得する
          let user = rec[2];

          //IDを取り出す
          let seatid = rec[0];

          //座席番号を取得する
          let seat = seatid.replace("seat","");

          //固定フラグがある場合の処理
          if(rec[1] == true){
            //ユーザ名を反映させる
            let textaddr = "#span" + seat;
            $(textaddr).html(user);
            
            //タイルのカラーを変更する(黄色)
            let gaddr = "#" + seat;
            $(gaddr).attr('fill', '#a6a6a6');
          }else{
            //割当ユーザがある場合の処理
            if(user != ""){
              //ユーザ名を反映させる
              let textaddr = "#span" + seat;
              $(textaddr).html(user);
              
              //タイルのカラーを変更する(黄色)
              let gaddr = "#" + seat;
              $(gaddr).attr('fill', '#fbffc7');
            }else{
              //次の処理へ
              //ユーザ名を消去する
              let textaddr = "#span" + seat;
              $(textaddr).html("");
              
              //タイルのカラーを変更する(デフォルト)
              let gaddr = "#" + seat;
              $(gaddr).attr('fill', '#afdde9');
            }
          }
        }

        //loaderを非表示にする
        $("#main").css("display", "block");
        $("#progress").css("display", "none");

      }

      //席のキープや情報ウィンドウを出す
      function seatinfo(seatid){
        //フラグ
        let kotei = false;
        let userflg = false;
        let userid = "";

        //seatidを格納する
        vm.seatid = seatid;

        //seatmanからidを検索して、固定席や割当ユーザがいるかチェック
        for(var i = 0;i<seatman.length;i++){
          //レコードを1個取り出す
          let rec = seatman[i];
          
          //idを取り出す
          let recid = rec[0];

          //IDと一致するかどうかチェック
          if(recid == seatid){
            //固定フラグの有無
            if(rec[1] == true){
              kotei = true;
              break;
            }else{
              if(rec[2] != ""){
                //ユーザが居るので、詳細ダイアログを出す
                userflg = true;
                userid = rec[3];
                break;
              }else{
                //ユーザがいないので、シート確保処理
                userflg = false;
                break;
              }
            }
          }else{
            //一致するのがないのでスルー
            continue;
          }
        }

        //koteiflgチェック
        if(kotei == true){
          //メッセージボックスを出して処理を終了
          vm.dialog = true;
          return;
        }else{
          if(userflg == true){
            //seatinfoに情報を追加
            vm.seatinfo = userid;

            //ユーザの詳細情報ダイアログを出す
            vm.dialog3 = true;
            return;
          }else{
            //座席キープの処理を実行
            vm.dialog2 = true;
            return;
          }
        }
      }

      //シートキープ後の処理
      function keepafter(keepflg){
        //keepflgに応じて処理を分岐
        switch(keepflg){
          case 0:
            snackman("席のキープに失敗しました。ユーザ登録が無いか?席情報の登録がありません。");
            break;
          case 1:
            snackman("席をキープしました。");
            //シートデータをリロードする
            google.script.run.withSuccessHandler(onSuccess2).seatload();

            //ダイアログを閉じる
            vm.dialog2 = false;
            break;
          case 2:
            snackman("すでに先に席がキープされてしまったようです。");
            break;
          case 3:
            snackman("座席はすでにキープ済みです。");
            //ダイアログを閉じる
            vm.dialog2 = false;
            break;
        }
      }

      //リリース処理後
      function releaseafter(rflg){
        switch(rflg){
          case 0:
            //リリース失敗
            snackman("リリースすべき席情報が見つかりませんでした。");
            break;
          case 1:
            snackman("席をリリースしました。");
            //シートデータをリロードする
            google.script.run.withSuccessHandler(onSuccess2).seatload();

            //ダイアログを閉じる
            vm.dialog4 = false;
            break;
        }
      }

      //snackメッセージを表示
      function snackman(msg){
				vm.text = msg;
				vm.snackbar = true;
			}

    </script>

  </head>
  <body onLoad="init()">
    <div id="progress" style="width: 100%; display:block">
      <center>
        <img src="https://officeforest.org/wp/library/loading.gif" border="0" alt="progress circle">
        <p><b>Now Loading ...</b></p>
      </center>
    </div>

    <div class="main" id="main" style="display:none">
      <v-app id="app">
        <v-toolbar dark	class="fixed-bar" src="https://picsum.photos/1920/1080?random">
					<v-toolbar-title class="titlehead">座席表アプリ</v-toolbar-title>
					<v-spacer></v-spacer>
					
					<v-btn icon @Click="onReturn">
						<v-icon color="white" class="tomato">mdi-train-car</v-icon>
					</v-btn>
				</v-toolbar>

        <!-- SVG画像貼り付け場所 -->
        <div id="gridarea" style="width: 100%; height: 300px;">
          <div id="map"></div>
        </div>

        <!-- 固定席メッセージ -->
        <v-dialog v-model="dialog" width="500">
          <v-card>
            <v-card-title class="text-h5 grey lighten-2">
              固定席ですよ!!
            </v-card-title>

            <v-card-text>
              <div>対象の席は固定席です。キープすることは出来ませんので、他の席を探して下さい。</div>
            </v-card-text>

            <v-divider></v-divider>

            <v-card-actions>
              <v-spacer></v-spacer>
              <v-btn color="primary" text @click="dialog = false">
                閉じる
              </v-btn>
            </v-card-actions>
          </v-card>
        </v-dialog>

        <!-- 座席キープダイアログ -->
        <v-dialog v-model="dialog2" width="500">
          <v-card>
            <v-card-title class="text-h5 grey lighten-2">
              座席のキープ
            </v-card-title>

            <v-card-text>
              <div>この席を今日はキープしますか?変える時には右上の帰宅のボタンを押して必ず座席のリリースを行って下さい。</div>
            </v-card-text>

            <v-divider></v-divider>

            <v-card-actions>
              <v-btn color="red" text @click="keepseat">
                キープ
              </v-btn>
              <v-spacer></v-spacer>
              <v-btn color="blue" text @click="dialog2 = false">
                閉じる
              </v-btn>
            </v-card-actions>
          </v-card>
        </v-dialog>

        <!-- ユーザ情報ダイアログ -->
        <v-dialog v-model="dialog3" width="500">
          <v-card>
            <v-card-title class="text-h5 grey lighten-2">
              ユーザの詳細情報
            </v-card-title>

            <v-card-text>
              <div>{{seatinfo}}についての詳細情報</div>
            </v-card-text>

            <v-divider></v-divider>

            <v-card-actions>
              <v-spacer></v-spacer>
              <v-btn color="primary" text @click="dialog3 = false">
                閉じる
              </v-btn>
            </v-card-actions>
          </v-card>
        </v-dialog>

        <!-- 座席リリースダイアログ -->
        <v-dialog v-model="dialog4" width="500">
          <v-card>
            <v-card-title class="text-h5 grey lighten-2">
              座席のリリース
            </v-card-title>

            <v-card-text>
              <div>今日はもうお帰りですか?リリースをクリックすると座席情報を開放して空にします。</div>
            </v-card-text>

            <v-divider></v-divider>

            <v-card-actions>
              <v-btn color="red" text @click="releaseseat">
                リリース
              </v-btn>
              <v-spacer></v-spacer>
              <v-btn color="blue" text @click="dialog4 = false">
                キャンセル
              </v-btn>
            </v-card-actions>
          </v-card>
        </v-dialog>

        <!-- メッセージ Toast-->
				<v-snackbar v-model="snackbar" :timeout="timeout">
					{{ text }}
					<template v-slot:action="{ attrs }">
            <v-btn color="blue"	text v-bind="attrs" @click="snackbar = false">
              閉じる
            </v-btn>
					</template>
				</v-snackbar>
      </v-app>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>

    <script>
			var vm;

      vm = new Vue({
        el: '#app',
        vuetify: new Vuetify(),
        data: () => ({
          snackbar:false,
          timeout:-1,
          text:"",
          seatinfo:"",
          seatid:"",        //座席クリック時のseatidを格納する用
          dialog:false,     //固定席ダイアログメッセージ用
          dialog2:false,    //席をキープするダイアログ用
          dialog3:false,    //ユーザの詳細情報ダイアログ用
          dialog4:false,    //席のリリース
        }),
        methods:{
          //座席キープ時処理
          keepseat(){
            google.script.run.withSuccessHandler(keepafter).onKeep(this.seatid);
          },

          //リサイズ時
          updateDataTableHeight() {
						setGridHeight();
					},

          //帰宅時の座席のリリースダイアログ
          onReturn(){
            this.dialog4 = true;
          },

          //座席のリリース処理
          releaseseat(){
            google.script.run.withSuccessHandler(releaseafter).onRelease();
          },
        },
        mounted : function(){

        }
			})		
    </script>
  </body>
</html>
  • シートデータロード時に固定席の場合には灰色キープ済みの席は黄色へカラー変更する為のコードが入っています。
  • Vue.jsおよびVuetifyを導入してUIを整えています。ツールバーには座席のリリース用ボタンを設置しています。
  • リリース後やキープ後には再度シートデータをリロードするようにしています。
  • UIを整える為のいくつかのVuetify用のCSSを追加しています。
  • 各種メッセージと確認用のダイアログ、スナックバーを追加しています。

ここに、データのリフレッシュ用の機能や、誰がどこに座っているのか検索する機能(検索後、タイルのstrokeカラーを赤にして場所を表示)といった機能を追加予定です。

図:座席のキープ時の問い合わせダイアログ

図:固定席はキープ出来なくしてあります

GAS側コード

今回追加したコードは以下の通り。トリガーの設置に関しては省略していますが、サンプルスプレッドシートには入っています。主に、シートデータを読み込んでHTML側へ返すコード、キープ時のコード、リリース時のコードになります。

//シートデータを取得する
function seatload(){
  //スプレッドシートのIDを取得する
  let Properties = PropertiesService.getScriptProperties();
  var ssid = Properties.getProperty("sheetid");

  //シートデータを取得する
  let sheet = SpreadsheetApp.openById(ssid).getSheetByName("シートデータ");
  let ss = sheet.getRange("A2:D").getValues();

  //シートデータを返す
  return JSON.stringify(ss);
}

//シートのキープ処理
function onKeep(seatid){
  //keepフラグ
  let keepflg = 0;
  let booking = false;

  //アクセスしてきてるユーザのアドレスを取得
  let user = GetUser();

  //スプレッドシートのIDを取得する
  let Properties = PropertiesService.getScriptProperties();
  let ssid = Properties.getProperty("sheetid");

  //シートデータを取得する
  let sheet = SpreadsheetApp.openById(ssid);
  let seatdata = sheet.getSheetByName("シートデータ").getRange("A2:D").getValues();
  let userdata = sheet.getSheetByName("ユーザリスト").getRange("A2:C").getValues();

  //ユーザリストに存在するかどうかチェック
  for(let i = 0;i<userdata.length;i++){
    //keepflgが1,2,3の場合場合ループを抜ける
    if(keepflg == 1 || keepflg == 2 || keepflg == 3){
      break;
    }

    //レコードを一個取り出す
    let urec = userdata[i];

    //ユーザアドレスが一致するかどうかチェック
    if(urec[1] == user){
      //ユーザ名とIDを取得
      let uid = urec[0];
      let uname = urec[2];

      //ユーザ登録があった場合
      for(let j = 0;j<seatdata.length;j++){
        //レコードを一個取り出す
        let srec = seatdata[j];

        //ユーザIDを取得する
        let srecid = srec[3];

        //すでに座席取得済みかどうかをチェックする
        if(srecid == uid){
          //座席取得済み
          booking = true;
          keepflg = 3;
          break;
        }
      }

      //座席まだ取得していない場合は取得処理をする
      if(booking == false){
        for(let k = 0;k<seatdata.length;k++){
          //レコードを一個取り出す
          let srec2 = seatdata[k];

          //seatidと一致するかどうか
          if(seatid == srec2[0]){
            //割当ユーザ列が空かどうかチェック
            if(srec2[2] == ""){
              //位置を特定
              let pos = k + 2;

              //空いてるのでキープ処理
              sheet.getSheetByName("シートデータ").getRange("C" + pos).setValue(uname);
              sheet.getSheetByName("シートデータ").getRange("D" + pos).setValue(uid);

              //フラグを変更
              keepflg = 1;
              break;
            }else{
              //すでに先に取られてる場合
              keepflg = 2;
              break;
            }
          }
        }
      }else{
        break;
      }
    }else{
      //一致しなかったのでスルーする
      continue;
    }
  }

  //フラグを元に答えを返す
  return keepflg;
}

//座席のリリース処理
function onRelease(){
  //フラグ
  let rflg = 0;

  //アクセスしてきてるユーザのアドレスを取得
  let user = GetUser();

  //スプレッドシートのIDを取得する
  let Properties = PropertiesService.getScriptProperties();
  let ssid = Properties.getProperty("sheetid");

  //シートデータを取得する
  let sheet = SpreadsheetApp.openById(ssid);
  let seatdata = sheet.getSheetByName("シートデータ").getRange("A2:D").getValues();
  let userdata = sheet.getSheetByName("ユーザリスト").getRange("A2:C").getValues();

  //ユーザデータを取得する
  for(var i = 0;i<userdata.length;i++){
    //レコードを一個取り出す
    let urec = userdata[i];

    //IDを取得する
    let uid = urec[0];

    //一致する情報を取り出す
    if(urec[1] == user){
      for(var j = 0;j<seatdata.length;j++){
        //レコードを一個取り出す
        let srec = seatdata[j];

        //idが一致するレコードを探索する
        if(srec[3] == uid){
          //位置を特定
          let pos = j + 2;

          //空いてるのでキープ処理
          sheet.getSheetByName("シートデータ").getRange("C" + pos).setValue("");
          sheet.getSheetByName("シートデータ").getRange("D" + pos).setValue("");

          //フラグ
          rflg = 1;
          break;
        }
      }
      //ループを抜ける
      break;
    }
  }

  //結果を返す
  return rflg;
}

//現在のユーザのアドレスを取得
function GetUser() {
  var objUser = Session.getActiveUser();
  return objUser.getEmail();
}

//深夜0時にデータをクリアするトリガー用関数
function clearsheet(){
  //シートIDを取得する
  var Properties = PropertiesService.getScriptProperties();
  var ssid = Properties.getProperty("sheetid");
  var ss = SpreadsheetApp.openById(ssid);
  
  //シートデータを取得する
  var sdata = ss.getSheetByName("シートデータ").getRange("A2:D").getValues();
  var slength = sdata.length;
  
  //ループを回して固定フラグ判定をしながらデータ消去
  for(var i = 0;i<slength;i++){
    if(sdata[i][1] == true){
      //固定席なので何もしない
    }else{
      //フリーアドレスなのでクリアする
      sdata[i][2] = "";
      sdata[i][3] = "";
    }
  }
  
  //配列データを一発貼り付け戻し
  var lastRow = sdata.length;      //レコードの数を取得する
  var lastColumn = sdata[0].length  //カラムの数を取得する
  ss.getSheetByName("シートデータ").getRange(2,1,lastRow,lastColumn).setValues(sdata);
}
  • すでにキープ時の場合は、エラーを返すようにしました。
  • 同一ユーザがキープ時の場合、ダブルブッキング防止の為にエラーを返すようにしました。
  • キープしていない場合は、スプレッドシートに表示名とIDを、自動取得したメアド(GetUser関数)に基づいて探索し、書き込みをするようになっています。
  • トリガーが24時にシートをクリアするコードを追加
  • リリース時はシートデータの該当のレコードから表示名とIDを消去します。

複数のSVG画像をまとめて取得

今回のサンプルの場合は1つのSVGファイルを取得して表示しているだけなので問題になることは無いのですが、複数のSVGファイルを切り替えて座席表を構築する場合、都度取得していてはロード時間分だけストレスです。しかしまとめて取得する場合DriveAppでその分だけリクエストを投げると、ロードが終わるまで結構時間が掛かります。

そこでUrlfetchApp.fetchAllDrive API V3を使って複数のSVG画像ファイルのIDをリクエストし、バッチリクエストで取得すると2倍程度早く取得することが可能です。baseurlには必ず「?alt=media」を加えないとデータそのものを取得することは出来ないので要注意。

function svgbatch(){
  //現在時刻を取得する
  let starttime = new Date();

  //シートIDを取得する
  let Properties = PropertiesService.getScriptProperties();
  var ssid = Properties.getProperty("sheetid");

  //SVGファイルID一覧を取得
  let ss2 = SpreadsheetApp.openById(ssid).getSheetByName("map");
  let sheet2 = ss2.getRange("A2:D").getValues();

  //各種配列
  let array = [];
  let dataman = [];

  //Drive API v3のベースURL
  let baseurl = "https://www.googleapis.com/drive/v3/files/"

  //まとめてリクエストを作成する
  for(var i = 0;i<sheet2.length;i++){
    let fileid = sheet2[i][3];

    if(fileid == ""){
      continue;
    }

    let req = {
        "url" : baseurl + fileid + "?alt=media",
        "headers": {
          "Authorization": "Bearer " + ScriptApp.getOAuthToken(),
          "contentType":"application/json",
        }
    }

    //配列に追加
    array.push(req);

    //datamanにデータを追加する
    let temparr = [
      sheet2[i][0],
      sheet2[i][1],
      sheet2[i][2],
      sheet2[i][3],
      ""
    ];

    dataman.push(temparr)
  }

  //fetchAllで一度にリクエスト
  var response = UrlFetchApp.fetchAll(array);

  //結果からSVGデータを取り出す
  for(var j = 0;j<response.length;j++){
    //レコードを1個取り出す
    let res = response[j].getContentText();
    
    //datamanの該当の箇所にSVGデータを入れる
    dataman[j][4] = res
  }

  //終了時刻との差分を算出して、メッセージを表示
  let endtime = new Date();
  let diff = (endtime.getTime()-starttime.getTime()) / 1000;

  console.log(diff)
}

3つほどのファイルの取得を試してみたところ、DriveAppで取得させる場合、2.75秒掛かっていました。Drive API v3でバッチリクエストの場合には、1.019秒程度。ファイルが多いほどこの差が堪えるので、複数ロードはバッチリクエストで呼んでしまうと良いでしょう。

表示サンプル

フルサイズはこちらをクリック

固定席(灰色)をクリックすると、エラーメッセージが出ます。また、誰かがキープ済みの席(黄色)は、そのユーザの情報を表示するダイアログを表示し、キープ済みではない場合には、座席取得の為のダイアログが表示するようになっています。すでに座席を自身でキープ済みの場合はダブルブッキングを阻止する仕組みが入っています。

帰宅時は右上のアイコンをクリックして座席のリリースをするようにします。

フリー素材を使ってみる

今回のサンプルを応用する事で、例えば会議室予約システムを構築したり、備品貸出システムを構築したり、またカレンダー式予約管理システムをかなりビジュアルで手軽に構築する事が可能になります。今回、SVG画像素材Library.comというサイトより素材をお借りして、作ってみました。東京都のベクター素材を利用してみたいと思います。

ただ、変換掛けると文字化けがするので、今回はこちらのサイトのPowerPointファイルを変換サイトでSVGに変換して使っています。Class指定、Fill・stroke・stroke-widthの指定、IDの指定等を加えています。

図:IDやスタイルの指定はInkscape上で完結します

GAS側コード

今回はGoogle DriveにアップロードしたSVG画像を直接ロードします。以下のようなコードで直接ファイルを読み込んでHTML側に反映することが可能です。SVG画像はUTF-8の文字コードになってるので注意が必要です。

直接ファイルをロードはDriveAppにてgetFileByIdに続いて、getBlobを取得し文字コード指定のgetDataAsStringで指定するだけ。中身はテキストなので、これだけで取得が可能です。

//SVGデータを取得して返す
function svgload(){
  //SVG画像ファイルのID
  let svgid = "ここにSVG画像のファイルのIDを記述する";

  //ドライブのSVGファイルの中身を直接取得する
  let svg = DriveApp.getFileById(svgid).getBlob().getDataAsString("utf-8");

  //HTML側へ取得内容を返す
  return svg;
}

HTML側コード

表示するだけで、図形に対して特に関数の割当等は行っていません。

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>

    <!-- Vuetify関係 -->
    <link href="https://fonts.googleapis.com/css2?family=M+PLUS+1p:wght@400;700&display=swap" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/MaterialDesign-Webfont/7.0.96/css/materialdesignicons.min.css" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">

    <style>
      svg {
        width: 100%;
        height:300px;
      }

      svg a path{
        transition: fill 0.3s linear;
      }

      .bar {
        pointer-events: all;
      }

      .bar:hover { 
        opacity: 0.7;
        fill: red;
      }

      #map{
        width:100%;
        height:1px;
      }
    </style>

    <style>
			.fixed-bar {
				position: sticky;
				position: -webkit-sticky; /* for Safari */
				top: 0em;
				z-index: 9;
			}
			html::-webkit-scrollbar{  /* メイン画面だけスクロールバーを消す */
				display: none;
			}

			html, body{				  /* メイン画面のスクロールを抑止 */
				overflow: hidden;
			}

			::-webkit-scrollbar{
				width: 10px;
			}
			::-webkit-scrollbar-track{
				background: #fff;
				border: none;
				border-radius: 10px;
				box-shadow: inset 0 0 2px rgb(54, 56, 53); 
			}
			::-webkit-scrollbar-thumb{
				background: rgb(65, 44, 255);
				border-radius: 10px;
				box-shadow: none;
			}

			.v-toolbar__extension{
				font-weight:bold;
				text-shadow:1px 1px 1px #000, -1px 1px 1px #000, 1px -1px 1px #000, -1px -1px 1px #000;
			}

			.titlehead{
				font-weight:bold;
				text-shadow:1px 1px 1px #000, -1px 1px 1px #000, 1px -1px 1px #000, -1px -1px 1px #000;
			}

			.tomato{
				text-shadow: 0 0 10px blue;
			}
		</style>

    <script>
      //グローバル変数
      var seatman = "";

      //リサイズ時にウィンドウにフィットさせる
			window.onresize = function(){
				setGridHeight();
			}
			  
			//gridのサイズを自動でウィンドウにフィットする
			$(document).ready(function () {
				setGridHeight();
			});

      //座席表の高さを自動補正する関数
			function setGridHeight() {
				var layoutHeight = $(window).height() - 25;
				$('#svg2').css('height', layoutHeight + 'px');
        $('#gridarea').css('height', layoutHeight + 'px');
			}

      //SVGファイルデータを呼び出し挿入する
      function init(){
        google.script.run.withSuccessHandler(onSuccess).svgload();
      }

      function onSuccess(data){
        //SVG画像を挿入する
        $("#map").html(data);
        setGridHeight();

        //loaderを非表示にする
        $("#main").css("display", "block");
        $("#progress").css("display", "none");

        snackman("SVG画像をロードしました。")
      }

      //snackメッセージを表示
      function snackman(msg){
				vm.text = msg;
				vm.snackbar = true;
			}

    </script>

  </head>
  <body onLoad="init()">
    <div id="progress" style="width: 100%; display:block">
      <center>
        <img src="https://officeforest.org/wp/library/loading.gif" border="0" alt="progress circle">
        <p><b>Now Loading ...</b></p>
      </center>
    </div>

    <div class="main" id="main" style="display:none">
      <v-app id="app">
        <v-toolbar dark	class="fixed-bar" src="https://picsum.photos/1920/1080?random">
					<v-toolbar-title class="titlehead">東京マップ</v-toolbar-title>
					<v-spacer></v-spacer>
				</v-toolbar>

        <!-- SVG画像貼り付け場所 -->
        <div id="gridarea" style="width: 100%; height: 300px;">
          <div id="map"></div>
        </div>

        <!-- メッセージ Toast-->
				<v-snackbar v-model="snackbar" :timeout="timeout">
					{{ text }}
					<template v-slot:action="{ attrs }">
            <v-btn color="blue"	text v-bind="attrs" @click="snackbar = false">
              閉じる
            </v-btn>
					</template>
				</v-snackbar>
      </v-app>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>

    <script>
			var vm;

      vm = new Vue({
        el: '#app',
        vuetify: new Vuetify(),
        data: () => ({
          snackbar:false,
          timeout:-1,
          text:"",
          seatinfo:"",
        }),
        methods:{

        },
        mounted : function(){

        }
			})		
    </script>
  </body>
</html>

表示サンプル

図:東京マップを描いてみた

フルサイズはこちらから開きます。

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)