【Maplibre】光害の地図を作った話

# はじめに
星空を眺めるときに空を明るく照らし暗い星を消してしまうことを「光害」と言う。光害の状況についてまとめたサイトとして、有名なものに[Light Pollution Map](https://www.lightpollutionmap.info/#zoom=8.11&lat=34.2024&lon=135.5681)がある。様々な機能があり非常に有用であるが、海外のサイトであり、日本の情報は見にくいと言う欠点があった。
そこで、日本の光害に関する情報をまとめるために[「日本光害地図 -Japan Linght Pollution Map」](https://astrotourism.jp/map/japan_lightpollution_map.html)を作成した。
星空を観察するときや星景写真、天体写真撮影時の参考になれば幸いです。

:::note info
ちなみに、20以上だと天の川が見やすいとされています[(環境省,2024)](https://www.env.go.jp/press/press_02365.html)。
:::


作成にあたりMaplibreやjavascript等の知識が不足している。これからも勉強していく必要があると感じたが、ゴールがあると勉強する気になり、良い機会であった。

https://astrotourism.jp/map/japan_lightpollution_map.html

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/928331/c8158040-3708-9566-a99c-51426b5cb5da.png)

※本記事は[観光情報学会 第26回研究発表会](http://www.sti-jpn.org/event/900)で発表した内容のうち、技術面について書いたものです。

# データについて
## 光害に関するデータ
### World Atlas 2015
世界中の光害についてまとめたデータ。夜間光をSuomi-NPP衛星のセンサ群VIIRSに搭載された可視近赤外センサDNB(Day/Night Band)にて計測、地上から測定したデータ等で補正したもの。
広域かつ均一なデータである一方、日本時間午前1時の計測である点や白色LEDの光を過小評価してしまうデメリットがある[(平松,2022)](https://note.com/parsonii/n/n90de6c1073da)。

License: [Attribution-NonCommercial 4.0 International (CC BY-NC 4.0)](https://creativecommons.org/licenses/by-nc/4.0/deed.ja)

https://cires.colorado.edu/Artificial-light

### 星空継続観察(環境省及び星空公団)
夏と冬に環境省が毎年実施している調査。デジタルカメラで夜空を撮影し、どのくらいの暗さなのかを測定している。実測であるため衛星からの観測より精度が高いと考えられる。
星空公団の実施したものを合わせると長期にわたり同一地点で計測したデータが得られる。一方、古いものは計測方法が変わっているため、直接の比較はできないことに注意が必要である。

ライセンス:[政府標準利用規約](https://www.digital.go.jp/assets/contents/node/basic_page/field_ref_resources/f7fde41d-ffca-4b2a-9b25-94b8a701a037/a0f187e6/20220706_resources_data_betten_01.pdf)

https://www.env.go.jp/air/life/hoshizorakansatsu/observe-4.html

## 公開天文台の地点について
星をみるために一般の人が訪れることのできる天文台。大きな望遠鏡で星を見せてもらえる。
[公開天文台白書2018](https://www.koukaitenmondai.jp/whitepaper/2018/index.html)のデータを使用した。

https://www.koukaitenmondai.jp/whitepaper/2018/index.html

## 背景地図
* [Open Street Map]()
* [国土地理院標準地図](https://maps.gsi.go.jp/development/ichiran.html)
* [写真画像(国土地理院)](https://maps.gsi.go.jp/development/ichiran.html)
* [白地図(国土地理院)](https://maps.gsi.go.jp/development/ichiran.html)
* [陰影起伏図(国土地理院)](https://maps.gsi.go.jp/development/ichiran.html)

Maplibreから使いやすい、タイル配信をしている地図を使用し、用途に応じて切り替えられるようにした。

# 仕様について
[Maplibre](https://maplibre.org/)と[maplibre-gl-opacity](https://github.com/mug-jp/maplibre-gl-opacity)プラグインを使用し、複数のデータを重ね合わせることや背景地図を切り替えられるようにした。
World Atlas2015のデータおよび公開天文台の地点データについてはPMtileにて、その他データはgeojsonで作成した。

<details><summary>実際のコード</summary>
恥ずかしいコードであるが、少しでもよくなることを目指し晒します。

```javascript:script.js
import OpacityControl from '/map/maplibre-gl-opacity.js';

		const protocol = new pmtiles.Protocol();
		maplibregl.addProtocol("pmtiles", protocol.tile);
		maplibregl.addProtocol("hakusyo", protocol.tile);
		maplibregl.addProtocol("misato_map", protocol.tile);

		const map = new maplibregl.Map({
			container: 'map', // div要素のid
			zoom: 8, // 初期表示のズーム
			center: [135.467861, 34.594436], // 初期表示の中心
			minZoom: 5, // 最小ズーム
			maxZoom: 18, // 最大ズーム
			pitch: 0,
			maxPitch: 85,
			bearing: 0,
			hash: false,
			localIdeographFontFamily: ['sans-serif'], // 日本語を表示するための設定
			style: {
				version: 8,
				glyphs: "https://glyphs.geolonia.com/{fontstack}/{range}.pbf",
				sources: {
					// 背景地図ソース
					osm: {
						type: 'raster',
						tiles: [
							'https://tile.openstreetmap.jp/styles/osm-bright-ja/{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 () {

			map.addSource(
				'chiriin_map', {
				type: 'raster',
				tiles: [
					'https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
				],
				tileSize: 256,
				attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
			}
			);
			map.addLayer({
				id: 'chiriin',
				source: 'chiriin_map',
				type: 'raster',
				minzoom: 0,
				maxzoom: 18,
				layout: {
					visibility: 'none'
				}
			},);

			map.addSource(
				'chiriin_map_2', {
				type: 'raster',
				tiles: [
					'http://cyberjapandata.gsi.go.jp/xyz/pale/{z}/{x}/{y}.png',
				],
				tileSize: 256,
				attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
			},
			);
			map.addLayer({
				id: 'chiriin_2',
				source: 'chiriin_map_2',
				type: 'raster',
				minzoom: 0,
				maxzoom: 18,
				layout: {
					visibility: 'none'
				}
			});

			map.addSource(
				'chiriin_map_3', {
				type: 'raster',
				tiles: [
					'https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg',
				],
				tileSize: 256,
				attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
			},
			);
			map.addLayer({
				id: 'chiriin_3',
				source: 'chiriin_map_3',
				type: 'raster',
				minzoom: 0,
				maxzoom: 18,
				layout: {
					visibility: 'none'
				}
			});

			map.addSource(
				'chiriin_map_4', {
				type: 'raster',
				tiles: [
					'https://cyberjapandata.gsi.go.jp/xyz/blank/{z}/{x}/{y}.png',
				],
				tileSize: 256,
				attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
			},
			);
			map.addLayer({
				id: 'chiriin_4',
				source: 'chiriin_map_4',
				type: 'raster',
				minzoom: 0,
				maxzoom: 14,
				layout: {
					visibility: 'none'
				}
			});

			map.addSource(
				'pmtiles', {
				type: 'raster',
				url: 'pmtiles://wa_2015_japan.pmtiles',
				minzoom: 1,
				maxzoom: 17,
				tileSize: 256,
				attribution: '<a href="http://doi.org/10.5880/GFZ.1.4.2016.001">World Atlas 2015</a>'
			});
			map.addLayer({
				id: 'hikarigai',
				source: 'pmtiles',
				type: 'raster',
				paint: {
					'raster-opacity': 0.5
				},
				layout: {
					visibility: 'none'
				},
			},);

			map.loadImage('observatory.png', (error, image) => {
				if (error) throw error;
				map.addImage('obs_icon', image);
			});

			map.addSource('obs_point', {
				type: 'geojson',
				data: 'hakusyo2018.geojson',
				attribution: "<a href='https://www.koukaitenmondai.jp/whitepaper/2018/index.html'>公開天文台白書2018</a>"
			});
			map.addLayer({
				id: 'obs_point',
				type: 'circle',
				source: 'obs_point',
				paint: {
					'circle-radius': 8,
					'circle-color': '#2b2b2b'
				}

				/*
				type: 'symbol',
				source: 'obs_point',
				layout: {
					'icon-image': 'obs_icon',
					'icon-size': 0.06,
				},
				*/
			});

			map.addSource("hillshade", {
				type: 'raster',
				tiles: [
					'https://cyberjapandata.gsi.go.jp/xyz/hillshademap/{z}/{x}/{y}.png',
				],
				attribution: '<a href="https://maps.gsi.go.jp/development/ichiran.html#hillshademap" target="_blank">地理院タイル(陰影起伏図)</a>',
				tileSize: 256
			});

			map.addLayer({
				id: 'hillshade-tiles',
				type: 'raster',
				source: 'hillshade',
				minzoom: 2,
				maxzoom: 18,
				paint: {
					'raster-opacity': 0.5
				},
				layout: {
					visibility: 'none'
				},
			});

			// 標高タイルソース
			map.addSource("tilezen-dem", {
				type: 'raster-dem',
				tiles: ['https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png'],
				attribution: '<a href="https://github.com/tilezen/joerd/blob/master/docs/attribution.md">Tilezen Joerd: Attribution</a>',
				encoding: "terrarium",
			});

			// 標高タイルセット
			map.setTerrain({ 'source': 'tilezen-dem', 'exaggeration': 1 });

			//SQMの計測データ
			map.addSource('sqm_result', {
				type: 'geojson',
				data: 'APIのURL',
			});

			map.addLayer({
				id: 'sqm_result',
				type: 'circle',
				source: 'sqm_result',
				layout: {
				},
				paint: {
					'circle-color': [
						"interpolate",
						["linear"],
						["get", "nsb"],
						0,
						"#ffffff",
						18,
						"#ff52ff",
						19,
						"#ff0004",
						19.5,
						"#ff9d00",
						20,
						"#fffb00",
						20.5,
						"#00ff40",
						20.75,
						"#00810b",
						21,
						"#007bff",
						21.25,
						"#0800ff",
						21.50,
						"#16285d",
						22,
						"#000000",
					],
					'circle-radius': 7
				},
			});

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

			});

			//環境省星空継続観察の結果
			map.addSource('hoshizora_keizoku_result', {
				type: 'geojson',
				data: 'hoshizora_keizoku.geojson',
				attribution: '<a href="https://www.env.go.jp/air/life/hoshizorakansatsu/index.html">環境省-星空観察-</a>'
			});

			map.addLayer({
				id: 'hoshizora_keizoku_result',
				type: 'circle',
				source: 'hoshizora_keizoku_result',
				layout: {
				},
				paint: {
					'circle-color': [
						"interpolate",
						["linear"],
						["get", "nsb"],
						0,
						"#ffffff",
						18,
						"#ff52ff",
						19,
						"#ff0004",
						19.5,
						"#ff9d00",
						20,
						"#fffb00",
						20.5,
						"#00ff40",
						20.75,
						"#00810b",
						21,
						"#007bff",
						21.25,
						"#0800ff",
						21.50,
						"#16285d",
						22,
						"#000000",
					],
					'circle-radius': 7
				},
			});

			map.addLayer({
				id: 'hoshizora_keizoku_result_label',
				type: 'symbol',
				source: 'hoshizora_keizoku_result',
				minzoom: 8,
				layout: {
					"text-field": ["get", "nsb"],
					'text-font': [
						'Noto Sans Regular',
					],
					'text-offset': [0, 1.25],
					'text-anchor': 'top',
					"text-size": [
						"interpolate",
						["linear"],
						["zoom"],
						10,//zoomレベル10の時
						10,//フォントサイズ8
						14,
						14
					],
				},
				paint: {
					"text-halo-width": 1,
					"text-halo-color": "#fff"
				},
			});

			//環境省星空継続観察の継続地点の結果
			map.addSource('keizoku_place_result', {
				type: 'geojson',
				data: 'keizoku_place.geojson',
				attribution: '<a href="https://www.env.go.jp/air/life/hoshizorakansatsu/index.html">環境省-星空観察-</a>'
			});

			map.addLayer({
				id: 'keizoku_place_result',
				type: 'circle',
				source: 'keizoku_place_result',
				layout: {
				},
				paint: {
					'circle-color': [
						"interpolate",
						["linear"],
						["get", "average"],
						0,
						"#ffffff",
						18,
						"#ff52ff",
						19,
						"#ff0004",
						19.5,
						"#ff9d00",
						20,
						"#fffb00",
						20.5,
						"#00ff40",
						20.75,
						"#00810b",
						21,
						"#007bff",
						21.25,
						"#0800ff",
						21.50,
						"#16285d",
						22,
						"#000000",
					],
					'circle-radius': 7
				},
			});

			map.addLayer({
				id: 'keizoku_place_result_label',
				type: 'symbol',
				source: 'keizoku_place_result',
				minzoom: 8,
				layout: {
					"text-field": [
						"concat", ["number-format",
							["get", "average"],
							{ 'min-fraction-digits': 2, 'max-fraction-digits': 2 }]],
					'text-font': [
						'Noto Sans Regular',
					],
					'text-offset': [0, 1.25],
					'text-anchor': 'top',
					"text-size": [
						"interpolate",
						["linear"],
						["zoom"],
						10,//zoomレベル10の時
						10,//フォントサイズ8
						14,
						14
					],
				},
				paint: {
					"text-halo-width": 1,
					"text-halo-color": "#fff"
				},
			});

			const mapBaseLayer = {
				'osm-layer': 'Open Street Map',
				'chiriin': '地理院地図',
				'chiriin_2': '淡色地理院地図',
				'chiriin_3': '衛星画像',
				'chiriin_4': '白地図',
			};

			const mapOverLayer = {
				"hillshade-tiles": "陰影起伏図",
				'hikarigai': '光害(World Atlas 2015)',
				"obs_point": "公開天文台",
				"sqm_result": "SQM測定結果",
				"sqm_result_label": "SQM測定値",
				"hoshizora_keizoku_result": "星空観察結果",
				"hoshizora_keizoku_result_label": "星空観察の値",
				"keizoku_place_result": "星空観察継続地点の結果",
				"keizoku_place_result_label": "星空観察継続地点の値",
			};

			const opacity = new OpacityControl({
				baseLayers: mapBaseLayer,
				overLayers: mapOverLayer,
				opacityControl: false,
			});
			map.addControl(opacity, 'top-right');

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

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

			map.addControl(
				new maplibregl.TerrainControl({
					source: 'tilezen-dem',
				})
			);
		});

		// 地物クリック時にポップアップを表示する
		map.on('click', 'obs_point', function (e) {
			var coordinates = e.features[0].geometry.coordinates.slice();
			var name = e.features[0].properties.正規化名称;
			var jyusyo = e.features[0].properties.正規化住所;
			console.log(name);

			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(name + "<br>" + jyusyo)
				.addTo(map);
		});

		// 地物クリック時にポップアップを表示する
		map.on('click', "sqm_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 nsb = e.features[0].properties.nsb;
			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>NSB : " + nsb + "<br>日時 : " + date + " (" + time + ")<br>備考 : " + memo)
				.addTo(map);
		});

		// 地物クリック時にポップアップを表示する
		map.on('click', "hoshizora_keizoku_result", function (e) {
			var coordinates = e.features[0].geometry.coordinates.slice();
			var town = e.features[0].properties.town;
			var datetime = e.features[0].properties.datetime;
			var nsb = e.features[0].properties.nsb;
			var note = e.features[0].properties.note;

			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("場所 : " + town + "<br>NSB : " + nsb + "<br>日時 : " + datetime + "<br>備考 : " + note)
				.addTo(map);
		});

		// 地物クリック時にポップアップを表示する
		map.on('click', "keizoku_place_result", function (e) {
			// エラーハンドリング
			if (!e.features || e.features.length === 0) {
				console.error('クリックされた地点にフィーチャーがありません');
				return;
			}

			var coordinates = e.features[0].geometry.coordinates.slice();
			var town = e.features[0].properties.town;
			var name = e.features[0].properties.name;
			var nsb = e.features[0].properties.average;

			let nsb_chart = [];
			nsb_chart.push(e.features[0].properties)

			delete e.features[0].properties.prif;
			delete e.features[0].properties.town;
			delete e.features[0].properties.name;
			delete e.features[0].properties.average;
			delete e.features[0].properties.x;
			delete e.features[0].properties.y;

			let label = Object.keys(nsb_chart[0]);
			let nsb_data = Object.values(nsb_chart[0])

			//console.log(label);
			//console.log(nsb_data);

			//グラフの描画準備
			const data = {
				labels: label,
				datasets: [
					{
						label: "NSB",
						data: nsb_data,
					},
				],
			};

			const config = {
				type: "line",
				data: data,
				options: {
					spanGaps: true
				},
			};

			while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
				coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
			}
			// ポップアップを表示する
			new maplibregl.Popup({
				offset: 10, // ポップアップの位置
				closeButton: true, // 閉じるボタンの表示
			})
				.setLngLat(coordinates)
				.setHTML("市町村 : " + town + "<br>場所 : " + name + "<br>平均NSB : " + nsb + "<br><div><canvas width='500' height='350'  id='hosizora'></canvas></div>")
				.addTo(map)

			setTimeout(function () {
				if (window.myChart instanceof Chart) {
					window.myChart.destroy();
				}
				window.myChart = new Chart(document.getElementById("hosizora"), config);
				console.log("グラフ描画");
			}, 500);

		});

```

</details>

# まとめ
日本の光害に関する情報を地図上に表示できた。星空を見に行く人の役に立つものになればうれしい。一方、まだまだ使い勝手が良いとは言えないので、Maplibreを勉強して改良していきたい。
最後になりましたが、光害やMaplibreなどの情報を公開してくださっている皆さまのおかげでこのサイトが作れました。感謝申し上げます。

## 今後改良したいところ（メモ）
* 日照時間の平年値を表示したい（≒晴れやすいところ）
* ベクトルタイルの背景に対応したい
* データPNG等を使用して、クリックしたところの光害の数値が出せるようにしたい
* 雨雲レーダーやひまわり赤外線画像が表示できるようにしたい
* 星空継続観察のデータは一気に出せるようにしたい

# 参考文献
## 光害、天文台関係

https://note.com/parsonii/n/n90de6c1073da

https://www.science.org/doi/10.1126/sciadv.1600377

https://www.env.go.jp/kids/star.html

https://www.env.go.jp/air/life/hoshizorakansatsu/observe-4.html

https://www.koukaitenmondai.jp/whitepaper/2018/index.html

## Maplibre関係

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

https://zenn.dev/asahina820/books/071ba23476fdc4

https://zenn.dev/shi_works/articles/8b2b71f7d3132c

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

https://github.com/shiwaku

https://www.openstreetmap.org/#map=6/37.71/136.82