【GAS】Googelスプレッドシートからgeojsonを出力し、MapLibreで表示してみた

# はじめに
「やりたいこと」を実現するため、いろんな人の情報を参考にし、作成しました。
不備・間違いの指摘やもっと良い方法等があれば、ご教示頂けますと幸いです。

## やりたいこと
<b>緯度・経度のついたデータをフォームを用いて収集し、地図表示したい</b>

## 上記を実現するための具体的な仕組み
[Googleフォーム](https://www.google.com/intl/ja_jp/forms/about/)に回答した結果（緯度・経度情報あり）を[Googleスプレッドシート](https://www.google.com/intl/ja_jp/sheets/about/)に反映し、[geojson](https://datatracker.ietf.org/doc/html/rfc7946)に変換。その後、地図ライブラリ（今回は[MapLibre](https://maplibre.org/)）を用い表示した。

# 収集したデータ例

緯度・経度と日時、数値データが含まれたものを想定。
[Googleフォームでの回答をGoogleスプレッドシートに連携させる](https://support.google.com/docs/answer/2917686?hl=ja#zippy=%2C%E5%9B%9E%E7%AD%94%E3%81%AE%E4%BF%9D%E5%AD%98%E5%85%88%E3%82%92%E9%81%B8%E6%8A%9E%E3%81%99%E3%82%8B)。

```text:data.csv
タイムスタンプ,name,place,緯度,経度,date,time,data,memo
2024/05/15 03:03:22,田中,大阪城,34.6872571,135.5258546,2024/05/09,20:50:00,35.43,	
2024/04/19 20:13:36,佐藤,姫路城,34.8394341,134.6913859,2024/09/11,21:15:00,19.17,

```

# Googleスプレッドシートのコード
スプレッドシートのApps scriptに下記を記入
WEBアプリとしてデプロイすると、そのURLがAPIの働きをする。その際、公開範囲を「全員」にしておくとWEBからもアクセスが可能（一方セキュリティ観点からはリスクがある）。

```javascript:spreadsheet_to_geojson.gs
function getData(sheetName) {
  //sheet名注意
  var sheet = SpreadsheetApp.getActive().getSheetByName("sheet1");
  //.slice(1)をつけることで、タイトル行を無視
  var rows = sheet.getDataRange().getValues().slice(1);

  //下記でjsonに含めたい項目を設定
  return rows.map(function(row) {
    var obj = {};
    obj.type = "Feature"
    obj.properties = {};
    obj.properties.timestamp = row[0];
    obj.properties.name = row[1];
    obj.properties.place = row[2];
    obj.properties.経度 = row[3];
    obj.properties.緯度 = row[4];
    //DateTime型は扱いにくい場合があるので、指定の形に整形
    obj.properties.date = Utilities.formatDate(row[5], 'JST', "yyyy/MM/dd");
    
    //””の時にエラーが起きるので、場合わけ処理
    if (row[6] == "") {
      //何も実行しない
    }else{
      obj.properties.time = Utilities.formatDate(row[6], 'JST', "HH:mm");
    }
    
    obj.properties.data = row[7];
    obj.properties.times = row[8];
    obj.properties.memo = row[9];

    obj.geometry = {};
    obj.geometry.type = "Point";
    obj.geometry.coordinates = [];
    obj.geometry.coordinates.push(row[4],row[3]);
    return obj;
  });
}

function doGet(e) {
  var data = getData('Sheet1');
  var geojson = {};
  geojson.type = "FeatureCollection";
  geojson.features = data;

  var callback = e.parameter.callback;
  
  if (callback) {
    return ContentService
    .createTextOutput(callback+'('+JSON.stringify(geojson, null, 2)+')')
    .setMimeType(ContentService.MimeType.JAVASCRIPT);
  } else {
     return ContentService
    .createTextOutput(JSON.stringify(geojson, null, 2))
    .setMimeType(ContentService.MimeType.JSON);
  }
}

```

# 地図に表示
MapLibreを使って、geojsonを地図表示した。
その際、各地点の数値に従い色分けと数値ラベルつけた。

```html:map.html
<!DOCTYPE html>
<html lang="ja">

<head>
	<meta charset="UTF-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1.0" />
	<script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>
	<link href="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css" rel="stylesheet" />
	<title>マップ</title>
</head>

<body style="margin: 0;">
	<div id="map" style="height: 100vh;"></div>
	<script type="module">

		const map = new maplibregl.Map({
			container: 'map', // div要素のid
			zoom: 8, // 初期表示のズーム
			center: [134.860814, 35.127726], // 初期表示の中心
			minZoom: 5, // 最小ズーム
			maxZoom: 18, // 最大ズーム
			pitch: 0,
			maxPitch: 85,
			bearing: 0,
			hash: true,
			localIdeographFontFamily: ['sans-serif'], // 日本語を表示するための設定
			style: {
				version: 8,
				glyphs: "https://glyphs.geolonia.com/{fontstack}/{range}.pbf",// 日本語を表示するための設定
				sources: {
					// 背景地図ソース（Open Street Map）
					osm: {
						type: 'raster',
						tiles: [
							'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png',
							'https://b.tile.openstreetmap.org/{z}/{x}/{y}.png',
						],
						tileSize: 256,
						attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
					},
				},
				layers: [
					// 背景地図レイヤー
					{
						id: 'osm-layer',
						source: 'osm',
						type: 'raster',
						minZoom: 0,
						maxZoom: 18,
					}
				]
			}
		});

		map.on('load', function () {
			// geojsonを表示する
			map.addSource('result', {
				type: 'geojson',
				data: 'Apps scriptのURLを入力',
			});

			map.addLayer({
				id: 'result',
				type: 'circle',
				source: 'result',
				layout: {
				},
				paint: {
					//dataの数値によってポイントの色を変える
					'circle-color': [
						"interpolate",
						["linear"],
						["get", "data"],
						0,　//dataの値が０の時
						"#ffffff",　//この色になる
						10,
						"#ff52ff",
						20,
						"#ff0004",
						30,
						"#ff9d00",
						40,
						"#fffb00",
						50,
						"#00ff40",
						60,
						"#00810b",
						70,
						"#007bff",
						80,
						"#0800ff",
						90,
						"#16285d",
						100,
						"#000000",
					],
					'circle-radius': 7 //ポイントの大きさ
				},
			});

			map.addLayer({
				id: 'result_label',
				type: 'symbol',
				source: 'result',
				minzoom: 8,
				//dataをポイントの下にラベル表示
				layout: {
					"text-field": ["get", "data"],
					'text-font': [
						'Noto Sans Regular',
					],
					'text-offset': [0, 1.25],
					'text-anchor': 'top',
					"text-size": [
						"interpolate",
						["linear"],
						["zoom"],
						10, //zoomレベル10の時
						10, //フォントサイズ8
						14, //zoomレベル14の時
						14 //フォントサイズ14
					],
				},
				paint: {
					"text-halo-width": 1,
					"text-halo-color": "#fff"
				},

			});

			// NavigationControl
			let nc = new maplibregl.NavigationControl();
			map.addControl(nc, 'top-left');

			// スケール表示
			map.addControl(new maplibregl.ScaleControl({
				maxWidth: 200,
				unit: 'metric'
			}));


			// 地物クリック時にポップアップを表示する
			map.on('click', "result", function (e) {
				var coordinates = e.features[0].geometry.coordinates.slice();
				var place = e.features[0].properties.place;
				var date = e.features[0].properties.date;
				var time = e.features[0].properties.time;
				var data = e.features[0].properties.data;
				var memo = e.features[0].properties.memo;

				while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
					coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
				}
				// ポップアップを表示する
				new maplibregl.Popup({
					offset: 10, // ポップアップの位置
					closeButton: false, // 閉じるボタンの表示
				})
					.setLngLat(coordinates)
					//ポップアップに表示する内容
					.setHTML("場所 : " + place + "<br>DATA : " + data + "<br>日時 : " + date + " (" + time + ")<br>備考 : " + memo)
					.addTo(map);
			});
		})
	</script>
</body>
</html>

```

# 参考
下記を大いに~~パク~~参考にさせていただきました。本当にありがとうございます。
* GAS関係

https://qiita.com/zero3/items/0a56be4f7dfc4b74ca88

https://qiita.com/phi/items/723aa59851b0716a87e3

* MapLibre GL 関係　#いつもお世話になってます。

https://zenn.dev/asahina820/books/c29592e397a35b

https://qiita.com/asahina820/items/7b432b94de55293737d6

https://github.com/shi-works

* 聖書、全人類買うべき本

https://www.shuwasystem.co.jp/book/9784798068923.html