【HERE】自分以外にもいま病んでる人がいるのを知ってちょっと安心できる地図つくをってみた

毎年、クリスマスの雰囲気にちょっとした抵抗感を感じ、ふんわりと病みがちになります。
[メンヘラテクノロジー](https://www.menhera-technology.com/)のらんらんと申します。

そんなクリスマスシーズン病みを少しでも癒すべく、漫画『ハルロック』に出てくる「ぼっち・ザ・LED」[^1]のオマージュアプリをつくってみました。

[^1]:「ぼっち」という言葉を含むツイートをしている人の場所を地図上に表示し、ぼっちは自分だけではないというのを可視化するツール

# 自分以外にもいま病んでる人がいるのを知ってちょっと安心できる地図

実際にできたWebアプリを簡単に紹介します。
まず、自分のいまの気分に合致するアイコンを選択します。
![スクリーンショット 2022-12-10 20.34.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180648/1f8d26a9-add9-b1ba-4f91-77ddd909febb.png)
「共有する」を押下すると、自分の現在地の位置情報にアイコンが地図上に表示され、同時に24時間以内に他の人たちが入力したアイコンも確認することができます。
![スクリーンショット 2022-12-10 20.35.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180648/f08805f6-e8ba-9c55-3d56-abc7d8189f93.png)
(いまはまだ私しか使っていないので、私が共有したアイコンのみが表示されています…)

## 開発環境
- Python 3.10.2
- Flask 2.2.2
- Flask-SQLAlchemy 3.0.2
- Flask-Marshmallow

## 使用API
- HERE Maps API for JavaScript
- GeoJSON

## ファイル構成
```
- app.py
- templates
    - index.html
    - here_map.html
- resources
    - emoji
    - base.css
- instance
    - db.todo
```

# つくってみた

以下が今回つくったWebアプリの全体イメージです。
![スクリーンショット 2022-12-10 22.33.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180648/08d93dcf-1cd1-3ace-129c-0697c85d45ae.png)

## HERE Maps API for JavaScriptを使ってみる

HERE Maps API for JavaScriptのマーカーオブジェクトを使用して、ユーザーが選択したアイコンを現在地に表示させました。

### APIキーの取得

まず、HERE Maps API for JavaScriptを使用するには、HERE platformのアカウントを作成し、APIキーの取得が必要です。

アカウント作成方法については以下の記事を参考にさせていただきました。

▼地図 位置情報サービス HERE 無料アカウント有効化手順
https://qiita.com/kekomat/items/d2e29bac84a27c1ec521

アカウントが作成できたら、「アクセスマネージャ」から「アプリ」タブを開き、「新しいアプリを登録」から必要情報を入力して、アプリを作成しました。
![スクリーンショット 2022-12-10 20.41.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180648/51b1b6fd-f318-8335-0775-4e8a86299e5b.png)

作成したアプリを開き、「資格情報」にある「APIキー」に移動し、「APIキーを作成」を押下しAPIキーを取得します。
![スクリーンショット 2022-12-10 20.44.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180648/c86f0159-d52c-6259-60bb-7ec70de9b73e.png)


### マーカーの表示

参考にさせてもらった公式のドキュメントは以下です。

▼チュートリアル - 日本のデータを使い始める
https://jp.developer.here.com/documentation/maps/3.1.32.0/dev_guide/topics/get-started-japan.html

▼オブジェクトをマップ - マーカーオブジェクト
https://jp.developer.here.com/documentation/maps/3.1.32.0/dev_guide/topics/marker-objects.html

まずは日本の地図で指定した緯度・経度に、指定したアイコンを表示させれるかを確認しました。ほとんどサンプルコードのままですが、APIキーと東京駅の緯度・経度（24.2867, 153.9807）に絵文字のアイコンを指定しました。

```html:here_map.html
<!doctype html>
<html>
	<head>
        <!-- HEREのモジュールの読み込み -->
		<meta name="viewport" content="initial-scale=1.0, width=device-width" />
      	<script src="https://js.api.here.com/v3/3.1/mapsjs-core.js" 
            type="text/javascript" charset="utf-8"></script>
      	<script src="https://js.api.here.com/v3/3.1/mapsjs-service.js"
            type="text/javascript" charset="utf-8"></script>
	</head>
	<body>
		<div style="width: 640px; height: 480px" id="mapContainer"></div>
		<script>
            // APIキーを設定し、オブジェクトを初期化
			var platform = new H.service.Platform({
			  'apikey': '（自分のAPIキー）'
			});

            // 地図の初期化
			var baseUrl = 'https://js.api.here.com/v3/3.1/styles/omv/oslo/japan/';
			var style = new H.map.Style(`${baseUrl}normal.day.yaml`, baseUrl);
			var omvProvider = new H.service.omv.Provider(omvService, style);
			var omvlayer = new H.map.layer.TileLayer(omvProvider, {max: 22});
			var map = new H.Map(
    			document.getElementById('mapContainer'),
    		 	omvlayer,
    		{
      			zoom: 12,
      			center: {lat: 24.2867, lng: 153.9807}  // 地図の中心地点の設定
    		});

            // マーカーの表示
            var lat = 24.2867;  // 東京駅の緯度
            var lng = 153.9807;  // 東京駅の経度
			var icon = new H.map.Icon('アイコンのファイル名');
            map.addObject(new H.map.Marker({lat:lat, lng:lng}, {icon: icon}));
		</script>
	</body>
</html>

```
このソースコードの緯度・経度とアイコンの種類を、ユーザーの入力値にあわせて変動させれるようにしていきます。

## Flaskでサーバーサイドをつくる

サーバーサイドの実装はFlaskを使い、データベースにはSQLAlchemyを使用しました。こちらも公式のドキュメントを参考に作成しました。

▼Flask-SQLAlchemy - クイックスタート
https://flask-sqlalchemy.palletsprojects.com/en/2.x/quickstart/

### データベースの作成

以下のようなデータ構造のテーブルを作成します。

- ID：自動で設定される番号（整数型）
- lat：緯度（浮動小数点型）
- lng：経度（浮動小数点型）
- emotion_no：アイコンの選定用（整数型）
- shered_at：共有時間（時間型）

```python:app.py
from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__, static_folder='resources')
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.todo'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

class MapData(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    lat = db.Column(db.Float)
    lng = db.Column(db.Float)
    emotion_no = db.Column(db.Integer)
    shered_at = db.Column(db.DateTime, nullable=False)
    
    def __init__(self, lat, lng, emotion_no, shered_at):
        self.lat = lat
        self.lng = lng
        self.emotion_no = emotion_no
        self.shered_at = shered_at

```

### POST・GETメソッド

次にPOST、GETを受け取ったときの処理を実装します。POSTの処理で現在地を取得する部分では、GeoJSONを使用しました。また、GETの処理でデータベースから取得したデータをJSON形式に整えるために、Flask-Marshmallowを使用しました。

▼GeoJSON
https://geojson.org/

▼Flask-Marshmallow
https://flask-marshmallow.readthedocs.io/en/latest/

```python:app.py
from flask import Flask, render_template, request, redirect, url_for,jsonify
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime, timedelta
import requests
from flask_marshmallow import Marshmallow

ma = Marshmallow(app)
class MapDataSchema(ma.SQLAlchemyAutoSchema):
    class Meta:
        model = MapData
        load_instance = True

@app.route('/')
def index():    
	return render_template('index.html')

@app.route('/mapdata', methods=['GET','POST'])
def map():
    #POSTの処理
    if request.method == 'POST':
        #現在地の取得  
        geo_request_url = 'https://get.geojs.io/v1/ip/geo.json'
        geo_data = requests.get(geo_request_url).json()
        lat = float(geo_data['latitude'])
        lng = float(geo_data['longitude'])

        #アイコンの指定
        emotion = request.form['emotion']
        emotion_no = emotion[-1]

        #現在時刻
        shered_at = datetime.now()

        #データベースに書き込み
        map_data = MapData(lat, lng, emotion_no, shered_at)
        db.session.add(map_data)
        db.session.commit()

        return render_template('here_map.html')
    
    #GETの処理
    elif request.method == 'GET':
        #現在時刻を取得
        today = datetime.now()
        yestaday = today - timedelta(days=1)

        #24時間以内のデータをデータベースから取得
        map_date_today = MapData.query.filter(MapData.shered_at > yestaday)

        #JSON形式に整形
        map_data_schema = MapDataSchema(many=True)
        map_data_json = map_data_schema.dump(map_date_today)

        print(map_data_json)

        return jsonify(map_data_json) 

```
はじめに表示される入力画面は以下のように実装しました。

入力フォームは以下のサイトを参考にCSSを作成しました。

▼ラジオボタン・チェックボタンの代わりに画像選択にする方法
https://0forest.com/css-raido-button/

```html:index.html
<!doctype html>
<html>
	<head>
		<title>いまの気分をきく画面</title>
		<link rel="stylesheet" href="/resources/base.css">
	</head>
	<body>
		<h1>自分以外にも病んでる人がいるのを知ってちょっと安心できる地図</h1>
        <!-- FlaskでPOSTを受けとるために、method="POST"を設定 -->
		<form action="/mapdata" method="POST">
			<p>いまの気分は？</p>
			<div class="sample-form">
                <!-- valueの値がFlaskに送られる入力値 -->
				<input id="image_a" type="radio" value="emotion1" name="emotion" checked>
				<label for="image_a"><img src="/resources/emoji/1.png" width="40" height="40"></label>
                <!-- 中略 -->
				<input id="image_l" type="radio" value="emotion12" name="emotion">
				<label for="image_l"><img src="/resources/emoji/12.png" width="40" height="40"></label>
			</div>
			<div>
				<input type="submit" value="共有する"/>
			</div>
		</form>
	</body>
</html>
```

## Flaskから受け取ったデータを地図上に表示させる

最後にFlaskからJsonデータを受け取り、HERE Maps API for JavaScriptを使用して、地図上にマーカーを表示させます。

```html:here_map.html
<script>
// 中略
var map = new H.Map(
	document.getElementById('mapContainer'),
	 omvlayer,
{
	  zoom: 12,
	  center: {lat: 35.68026, lng: 139.76744}
});

//　↑ここまでは先ほどと同じ

fetch("/mapdata")
	.then((res)=>{
	return(res.json());  //JSONデータを取得
	})
	.then((json)=>{
		let result = JSON.stringify(json);
		let map_data = JSON.parse(result);
		map_data.forEach(function(value) {  //順番に地図上にアイコンを反映
			map.addObject(
				new H.map.Marker({lat: value.lat, lng: value.lng}, 
				{ icon: new H.map.Icon(`/resources/emoji/${value.emotion_no}.png`)}
				));
		});
	});
</script>
```

# おわりに

今回、[アドベントカレンダー](https://qiita.com/advent-calendar/2022/here)に参加し、以前から気になっていた地図情報のAPIを触る良い機会となりました。普段はPythonを使ったデータ分析系の実装しかしないので、苦戦した部分も多かったですが、フロントエンドの知識が乏しい私でもHERE Maps API for JavaScriptはかなりお手軽に実装できた印象でした。

本当はもっと可愛い見た目のWebアプリに仕上げたかったのですが、時間＆スキルが足りなかったので、社内のフロントエンドエンジニアを召喚して作りきれたらなと思っています。実装部分もかなり怪しいところが多いので、そこもあわせて改修していきたい所存です。

Webアプリとして公開できるようになったらまたどこかで共有したいと思います！