【exif-js】複数のGPS付き写真を地図上に表示したい(mapbox)

html

あー,どうもこんにちは.

今日はmapbox上に地図データを追加するコードを書いていきたいと思います.今回はmapboxを使いますが,座標の取り方とかは地図に依存しないので,他の地図でも使えるかもですね.

撮った写真を地図上で整理したい時ありますよね.
今回はjavascriptを使うので,一時的に表示するものです.これどこで撮ったっけとか,位置をもとに確認したい時に使ってもらうのが良いかと思います.

作成する機能

必要な機能は3つかあるので,それぞれに分けて書いていきます.

  1. htmlにファイルをアップロードする機能
  2. 写真の位置情報を取得する機能
  3. 位置情報をもとに地図上に表示する機能

ちょっと長いので二つくらいに分けたお話になるのかなと思います.

はじめに

まず,基礎となるhtmlを書きたいと思います.所謂mapboxを始めるためのコードです.
mapboxのアクセストークンの作り方とかは簡単に見つかると思うので,調べてください.
画像の読み込みはexif-jsを使います.

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>map</title>
	<script src='https://api.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.js'></script>
	<link href='https://api.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.css' rel='stylesheet' />
	<!-- 画像を読み込むための -->
	<script src="https://cdn.jsdelivr.net/npm/exif-js"></script>
</head>
<body>
	<div id='map' style="height: 100vh;"></div>
</body>
<script type="text/javascript">
	mapboxgl.accessToken = 'アクセストークン(これは作ってください)';
	var map = new mapboxgl.Map({
		container: 'map',
		style: 'mapbox://styles/mapbox/satellite-v9', 
				center: [136, 35],
				zoom: 9,
				pitch: 30,
				antialias: true
	});
</script>
</html>
Code language: HTML, XML (xml)

フォームを作る

これは他の記事にも書いたと思いますが,mapbox上にフォームを作成していきます.今回は複数の写真をまとめていきたいと思いますので,フォルダごとアップロードできるフォームを作ります.

想定フォルダ

├── img
            ├── img_1.jpg
           ├── img_2.jpg
           └── img_3.jpg
Code language: CSS (css)

これをそのままアップロードすれば勝手に動くやつを作りたいと思います.

あっ,ちなみに,「アップロード」というのは,写真をサーバーに上げるわけではなく,公開する恐れはないのでご安心ください.

なのでサーバーとかも全く入りません.htmlファイルとブラウザ(Googleなど)があれば無問題です.

プログラム

以下のlabelタグの部分ですね.これをhtmlに入れればいい感じにフォルダができます.
今回は適当にclassをつけたdivの中に入れました.

htmlタグとかheadタグはスルーしています.(最後の記事でまとめたやつを付ける予定です(予定))
ハイライトした部分がフォームです.

<body>
	<div class="map-overlay top">
		<div class="map-overlay-inner">
			<div id="" style='text-align: center;'>
				<label for='InputFiles'>
					ディレクトリを選択してください。
					<input id="InputFiles" type="file" webkitdirectory>
				</label>
			</div>		
		</div>
	</div>
</body>

<!-- ここからはstyle -->
<style>
	body { margin: 0; padding: 0; }
	.marker {
		background-color: red;
		background-size: cover;
		width: 30px;
		height: 30px;
		border-radius: 50%;
		cursor: pointer;
	}
	.map-overlay {
		font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
		position: absolute;
		width: 30%;
		top: 0;
		left: 0;
		padding: 10px;
	}
	
	.map-overlay .map-overlay-inner {
		background-color: #fff;
		box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
		border-radius: 3px;
		padding: 10px;
		margin-bottom: 10px;
	}
</style>
Code language: HTML, XML (xml)

こんな感じ

上のコードにmapboxとか追加するとこんな感じです.これでhtmlにファイルごとアップロードする機能ができました.

フォームを入れてみた

フォルダの中の写真とその位置情報の取得

続いては先ほどのフォームから取得したフォルダの中の写真とその位置情報を取得していきます.

exif-js

まず,headタグでexif-jsのスクリプトを読み込みます.説明が遅れましたが今回はexifを使って写真の位置情報を取得します.

最初に書いた部分なので再掲.

<head>
	<script src="https://cdn.jsdelivr.net/npm/exif-js"></script>
</head>Code language: HTML, XML (xml)

formとの関連付け

これだと特に何も結果は現れないのですが,とりあえず結果を読み込んでいます.

DirInputTag.addEventListenerは「フォームからフォルダを読み込んだとき」に動く関数を示しています.
8行目のresultでは画像を格納しています.一方で10行目のfileをURLとして読み込む必要もあります.ちょっと今は何をしていたかあんまり覚えてないんですが,どっちも不可欠です笑

<script>
	var DirInputTag = document.getElementById('InputFiles');
	var files;
	DirInputTag.addEventListener('change', function(e){
		files = e.target.files;
		for (var i=0; i<files.length; i++) {
			let reader = new FileReader();
			var result = '';
			reader.onload = function(e) {
				result = e.target.result;
			}
			reader.readAsDataURL(files[i]);
		}
	});
</script>
Code language: HTML, XML (xml)

位置情報の取得

先ほどのjsに追記していきます.こっからはややこしいので頑張ってください.
ハイライトした部分を追加しました.あまり詳しくは触れませんが,latやlonで緯度や経度を抽出しています.元々は「度」単位なので単位変換をしています.15行目のif~で「GPS座標がある場合は」という条件分岐を入れているので,座標がついていないものは勝手に省く設定にしています.

DirInputTag.addEventListener('change', function(e){
		files = e.target.files;
		var img
		var div;
		for (var i=0; i<files.length; i++) {
			let reader = new FileReader();
			var result = '';
			reader.onload = function(e) {
				// img = document.createElement('img');
				// img.setAttribute('src', e.target.result);
				result = e.target.result;
			}
			reader.readAsDataURL(files[i]);
			EXIF.getData(files[i],function(){
				if (EXIF.getTag(this, "GPSLatitude")) {
					// EXIF.getTag(this, "[exifのタグ名]")で、値を取得
					var lat = parseFloat(EXIF.getTag(this, "GPSLatitude")[0]) + (parseFloat(EXIF.getTag(this, "GPSLatitude")[1]) /60) + (parseFloat(EXIF.getTag(this, "GPSLatitude")[2]) /3600);
					var lon = parseFloat(EXIF.getTag(this, "GPSLongitude")[0]) + (parseFloat(EXIF.getTag(this, "GPSLongitude")[1]) /60) + (parseFloat(EXIF.getTag(this, "GPSLongitude")[2]) /3600);
					var coord = [lon,lat]
					var name = this.name;
					//ここからmapboxの表示に値を代入していく
				}
			})
		}
	});
Code language: JavaScript (javascript)

これで,座標(var cood)と画像(result)と画像名(name)を取得することができました.
あとはmapboxに表示する作業を進めていきます.

mapboxに連携

さてさらにコードを書き進めていきます.

今回はmapboxのmarker機能を使って,それをクリックするとpopupが出るというスタイルでいきます.
先ほどのコードにハイライトした部分を追加しています.まぁちょっと頑張って解読してほしい.

32行目のel.style.background…のコメントアウトを解除すると,マーカーの背景に画像がつきます.画像の枚数が増えると重くなるので一旦やめました.画質とか操作すれば少しは変わるかもですね!(僕はやらない)

<script>
	DirInputTag.addEventListener('change', function(e){
		files = e.target.files;

		var img
		var div;
		for (var i=0; i<files.length; i++) {
			let reader = new FileReader();
			var result = '';
			reader.onload = function(e) {
				// img = document.createElement('img');
				// img.setAttribute('src', e.target.result);
				result = e.target.result;
			}
			// reader.readAsDataURL(files[i]);
			EXIF.getData(files[i],function(){
				if (EXIF.getTag(this, "GPSLatitude")) {
					// EXIF.getTag(this, "[exifのタグ名]")で、値を取得
					var lat = parseFloat(EXIF.getTag(this, "GPSLatitude")[0]) + (parseFloat(EXIF.getTag(this, "GPSLatitude")[1]) /60) + (parseFloat(EXIF.getTag(this, "GPSLatitude")[2]) /3600);
					var lon = parseFloat(EXIF.getTag(this, "GPSLongitude")[0]) + (parseFloat(EXIF.getTag(this, "GPSLongitude")[1]) /60) + (parseFloat(EXIF.getTag(this, "GPSLongitude")[2]) /3600);
					var coord = [lon,lat]
					var name = this.name;

					// create a HTML element for each feature
					const el = document.createElement('div');
					el.className = 'marker';
					const popup = new mapboxgl.Popup({ offset: 20 }).setHTML(
						'<img width="150px",height="150px" src="'+result+'" style="object-fit:cover;"><p>' + name + '</p>'
					);

					new mapboxgl.Marker().setLngLat(coord).setPopup(popup).addTo(map);
					// el.style.backgroundImage = 'url(' + result + ')';めっちゃ重くなる
				}
			})
		}
	});
</script>
Code language: HTML, XML (xml)

と,言うわけでこれにて終了ですね.

完成版

最後に完成版を載せてこの記事を終えようと思います.

ただし,このコードは背景地図を選択できる機能が追加されています笑
一時的に写真の位置を確認したい場合には非常に使える機能だと思いますので,是非ご利用ください.

<!DOCTYPE html>
<html>
<head>
	<title>sample map</title>
	<script src='https://api.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.js'></script>
	<link href='https://api.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.css' rel='stylesheet' />
	<!-- 画像を読み込むための -->
	<script src="https://cdn.jsdelivr.net/npm/exif-js"></script>
</head>

<!-- html追加部分 start-->
<body>
	<div id='map' style="height: 100vh;"></div>
	<div class="map-overlay top">
		<div class="map-overlay-inner">
			<div id="" style='text-align: center;'>
				<label for='InputFiles'>
					ディレクトリを選択してください。
					<input id="InputFiles" type="file" webkitdirectory>
				</label>
			</div>		
		</div>
		<div class="map-overlay-inner">
			<div id="menu">
				<input id="satellite-v9" type="radio" name="rtoggle" value="satellite" checked="checked">
				<label for="satellite-v9">satellite</label>
				<label for="light-v10">light</label>
				<input id="dark-v10" type="radio" name="rtoggle" value="dark">
				<label for="dark-v10">dark</label> -->
				<input id="streets-v11" type="radio" name="rtoggle" value="streets">
				<label for="streets-v11">streets</label>
				<input id="outdoors-v11" type="radio" name="rtoggle" value="outdoors">
				<label for="outdoors-v11">outdoor</label>
				<input id="satellite-streets-v11" type="radio" name="rtoggle" value="satellite-streets-v11">
				<label for="satellite-streets-v11">satellite-street</label>
			</div>	
		</div>
	</div>
</body>
<!-- html追加部分 end-->

<script type="text/javascript">
	mapboxgl.accessToken = 'アクセストークン!';
	var map = new mapboxgl.Map({
		container: 'map',
		style: 'mapbox://styles/mapbox/satellite-v9', // マップのスタイル(デザイン)
				center: [136.1730, 35.850],
				zoom: 9,
				pitch: 30,
				antialias: true
	});
	map.addControl(new mapboxgl.NavigationControl());

        const layerList = document.getElementById('menu');
        const inputs = layerList.getElementsByTagName('input');
        
        for (const input of inputs) {
            input.onclick = (layer) => {
                const layerId = layer.target.id;
                map.setStyle('mapbox://styles/mapbox/' + layerId);
            };
        }
	map.on('load',() => {});
	map.on('style.load', () => {
		_addSource(map);
		_addLayer(map);
	});

	var DirInputTag = document.getElementById('InputFiles');
	var files;
	var file;

	DirInputTag.addEventListener('change', function(e){
		files = e.target.files;

		var img
		var div;
		for (var i=0; i<files.length; i++) {
			let reader = new FileReader();
			var result = '';
			reader.onload = function(e) {
				result = e.target.result;
			}
			reader.readAsDataURL(files[i]);
			EXIF.getData(files[i],function(){
				if (EXIF.getTag(this, "GPSLatitude")) {
					// EXIF.getTag(this, "[exifのタグ名]")で、値を取得
					var lat = parseFloat(EXIF.getTag(this, "GPSLatitude")[0]) + (parseFloat(EXIF.getTag(this, "GPSLatitude")[1]) /60) + (parseFloat(EXIF.getTag(this, "GPSLatitude")[2]) /3600);
					var lon = parseFloat(EXIF.getTag(this, "GPSLongitude")[0]) + (parseFloat(EXIF.getTag(this, "GPSLongitude")[1]) /60) + (parseFloat(EXIF.getTag(this, "GPSLongitude")[2]) /3600);
					var coord = [lon,lat]
					var name = this.name;

					// create a HTML element for each feature
					const el = document.createElement('div');
					el.className = 'marker';
					const popup = new mapboxgl.Popup({ offset: 20 }).setHTML(
						'<img width="150px",height="150px" src="'+result+'" style="object-fit:cover;"><p>' + name + '</p>'
					);

					new mapboxgl.Marker().setLngLat(coord).setPopup(popup).addTo(map);
					// el.style.backgroundImage = 'url(' + result + ')';めっちゃ重くなる
				}
			})
		}
	});
</script>
<style>
	body { margin: 0; padding: 0; }
	.marker {
		background-color: red;
		background-size: cover;
		width: 30px;
		height: 30px;
		border-radius: 50%;
		cursor: pointer;
	}
	.mapboxgl-popup {
		max-width: 300px;
	}
	.map-overlay {
		font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
		position: absolute;
		width: 30%;
		top: 0;
		left: 0;
		padding: 10px;
	}
	
	.map-overlay .map-overlay-inner {
		background-color: #fff;
		box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
		border-radius: 3px;
		padding: 10px;
		margin-bottom: 10px;
	}
</style>
</html>
Code language: HTML, XML (xml)

コメント

タイトルとURLをコピーしました