【HERE】指定の緯度経度から初日の出の見えるホテルを探してみる

今年も早いもので、あと少しで一年も終わります。

初日の出は、日本では長い歴史を持つ伝統的な行事です。
日本人にとっては、新年を迎える前に、精神的にリフレッシュし、新しい一年に向けて前向きな気持ちになるために重要な行事です。
また、初日の出を見ることで、人々は自然の美しさや宇宙の秘密を感じることができます。ホテルから初日の出が見られる場所や屋上から初日の出を眺めることで、新年を迎える際に、より贅沢な気分で新年を迎えることができます。

そんな、初日の出の見られるホテルを予約したりして、贅沢な新年を迎えたいと思い、初日の出の見えるホテルを機械学習で、探してみました。

# 初日の出の見えるホテルの地図
こちらは作ったプログラムから出力されたCSVを[【HERE Studio】](https://jp.developer.here.com/documentation/platform_studio/user_guide/index.html)で表示したものになります。
地図表示は[こちら](https://platform.here.com/studio/viewer?project_id=fstsunrisehotel&artifact_hrn=hrn:here:artifact::org425938302:com.here.studio.fstsunrisehotel:project-api:1.0.0&apiKey=3jFeyk9LcIKtRPIoFpFy4SeTrigPBOE6XcsO0UMG3no)から

[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20464/a57a24d5-a150-f43a-31bc-e04cf2a517eb.png)](https://platform.here.com/studio/viewer?project_id=fstsunrisehotel&artifact_hrn=hrn:here:artifact::org425938302:com.here.studio.fstsunrisehotel:project-api:1.0.0&apiKey=3jFeyk9LcIKtRPIoFpFy4SeTrigPBOE6XcsO0UMG3no)

こちらは、伊豆の南側周辺のホテルに対しての初日の出の見える度判定を行っております。
赤いアイコンは初日の出の見えないホテル、青いアイコンは初日の出の見えるホテルを表しています。

こうして、結果を見てみると日の出の角度から南東の方向のホテルが初日の出の見えるホテルになっているのがわかります。山の中に見える青いアイコンのホテルは標高が高くてこの位置からだと、初日の出が見えるということも判定結果から導き出されていますね。
※実際は木々や建物など障害物もあるかもしれませんので、詳細は個別に調べた方がよい部分もあります。

## 開発環境
* Java 11
* Maven
* Jackson
* OpenCSV

## 使用API
* [HERE Geocoding & Search API](https://jp.developer.here.com/documentation/geocoding-search-api/dev_guide/index.html) 
* [朝夕日和 API](https://protopedia.net/material/888)(自作)

# 作った内容

今回は以下の内容で、処理をしています。
1. 【HERE Geocoding & Search API】を利用して、緯度経度の近くのホテルを取得する。
2. 取得したホテルに対して、【朝夕日和 API】の「朝日と夕日の見える度判定」を利用して、初日の出の見える度判定を行う。
3. 判定したホテル一覧をCSVに出力する。

## 1. 緯度経度の近くのホテルを取得する
最初に【HERE Geocoding & Search API】を利用するためには、HERE platformのアカウントを作成し、APIキーの取得が必要になりますので、こちらに記事をリンクしておきます。

https://qiita.com/kekomat/items/d2e29bac84a27c1ec521


### APIキーの取得
プラットフォームの画面から「REST APIて始める」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20464/d9517415-48de-51a9-d6f9-6766770a76a5.png)
「アクセスマネージャー」の部分をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20464/6946daa4-921a-808d-243a-03364389336b.png)
「新しいアプリを登録」が表示されたら、アプリ情報を入力し、登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20464/384b33d1-240b-c842-a510-b4a61ae4f18a.png)
作成したアプリを開き、「資格情報」にある「APIキー」に移動し、「APIキーを作成」を押下しAPIキーを取得します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20464/2dcba992-fd98-c421-38c3-3d890a6a81be.png)

### REST APIをJavaで取得する
【HERE Geocoding & Search API】の中で、「[場所の近くのカテゴリで場所を検索します](https://jp.developer.here.com/documentation/geocoding-search-api/dev_guide/topics-api/code-discover-ontology-location.html)」のAPIを利用しています。

以下の内容をブラウザに入力して、JSONの内容を取得します。
```md:GET
https://discover.search.hereapi.com/v1/discover?at=34.6202363,138.8274184&limit=10&lang=ja&q=ホテル&apiKey=<<----API KEY---->>
```

```md:JSONの内容
{
    "items": [
        {
            "title": "大正丸",
            "id": "here:pds:place:392xn4sz-3fcf4c2d8082c88f7e564188c17573d7",
            "language": "ja",
            "ontologyId": "here:cm:ontology:hotel",
            "resultType": "place",
            "address": {
                "label": "〒415-0311 静岡県賀茂郡南伊豆町中木1628 大正丸",
                "countryCode": "JPN",
                "countryName": "日本",
                "state": "静岡県",
                "county": "賀茂郡",
                "city": "南伊豆町",
                "subdistrict": "中木",
                "subblock": "1628",
                "postalCode": "415-0311"
            },
            "position": {
                "lat": 34.61574,
                "lng": 138.82514
            },
            "access": [
                {
                    "lat": 34.61574,
                    "lng": 138.82514
                }
            ],
            "distance": 542,
            "categories": [
                {
                    "id": "500-5100-0057",
                    "name": "ゲストハウス",
                    "primary": true
                },
                {
                    "id": "500-5100-0061",
                    "name": "旅館"
                }
            ],
            "references": [
                {
                    "supplier": {
                        "id": "tripadvisor"
                    },
                    "id": "8547092"
                }
            ],
            "contacts": [
                {
                    "phone": [
                        {
                            "value": "+81558651007"
                        }
                    ]
                }
            ]
        }
    ]
}
```

こちらの記事を参考にJSONの内容からモデルクラスを自動生成をします。

https://qiita.com/riversun/items/cdd990e96c2bbf9cb043

自動生成したモデルクラスを利用したJacksonでのモデルクラスの変換
```java
    private List <Item> getHotel(final String latlng, final String limit, final String apiKey) throws IOException {

        // URLを生成
        final var sb = new StringBuilder("https://discover.search.hereapi.com/v1/discover");
        sb.append("?at=" + latlng);// 緯度経度を指定
        sb.append("&limit=" + limit);// 件数を指定
        sb.append("&lang=ja");// 言語に日本語を指定
        sb.append("&q=ホテル");// カテゴリーにホテルを指定
        sb.append("&apiKey=" + apiKey); // APIキーを指定

        // URLを保持する適切な値オブジェクトを作成する
        final var url = new URL(sb.toString());

        // URLで接続を開き、コネクションを取得する
        final var connection = (HttpURLConnection) url.openConnection();

        // インプットストリームを取得する
        try (final var reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)) {

            final var mapper = new ObjectMapper();
            final var model = mapper.readValue(reader, DiscoverModel.class);

            return model.items;
        }
    }

```

## 2. 初日の出の見える度判定を行う
次に取得したホテルの一覧から初日の出の見える度判定を行っていきます。
![スライド4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20464/8dc87adf-7861-74b9-2bc4-7f1218c57d86.png)
以下の内容をブラウザに入力して、JSONの内容を取得します。
```md:GET
https://livlog.xyz/asayuubiyori/api/sceneryJudgment?year=2023&month=1&day=1&lat=34.6202363&lng=138.8274184&height=0
```

```md:JSONの内容
{
    "results": [
        {
            "date": {
                "month": 1,
                "year": 2023,
                "day": 1
            },
            "sunriseElevation04": -1,
            "sunriseElevation16": -1,
            "sunsetRate": 0.6192,
            "sunriseElevation01": 135.3,
            "sunriseElevation12": -1,
            "sunriseElevation02": 27.4,
            "sunsetElevation08": -1,
            "sunriseRate": 0,
            "positions": {
                "sunsetAzimuth": 242.31500733835114,
                "moonsetAltitude": 0.56422837979349,
                "solarNoonAzimuth": 180.01661404543083,
                "sunriseAzimuth": 117.75391682573763,
                "moonriseAzimuth": 74.38486878874016,
                "moonsetAzimuth": 282.52209717467224,
                "sunsetAltitude": -0.8679911972409857,
                "moonriseAltitude": 0.5919307494238234,
                "sunriseAltitude": -0.8323991704209387,
                "solarNoonAltitude": 32.37880614617465
            },
            "sunsetElevation16": -1,
            "sunsetElevation04": -1,
            "sunsetElevation02": -1,
            "sunsetElevation01": 84.8,
            "sunsetElevation12": -1,
            "baseElevation": 72.9,
            "location": {
                "coordinate": {
                    "lng": 138.8274184,
                    "lat": 34.6202363
                }
            },
            "sunriseElevation08": -1,
            "riseAndSet": {
                "sunriseHm": "6:51",
                "sunsetHm": "16:44",
                "solarNoonHm": "11:48",
                "moonsetHm": "1:24",
                "moonriseHm": "12:40"
            }
        }
    ],
    "status": 0
}
```

こちらも「REST APIをJavaで取得する」と同じようにJSONの内容からモデルクラスを自動生成をします。

そして、こんな感じで、初日の出の見える度判定を実装しています。
こちらのAPIは[朝夕日和](https://asayuubiyori.hoshi-miru.com/)のアプリを作成時に作ったものです。

```java
    private Result getSceneryJudgment(final Item item) throws IOException {

        if (item.access.isEmpty()) {
            return null;
        }

        // 2023年の元旦の日付を設定
        final var year = "2023";
        final var month = "1";
        final var day = "1";

        // URLを生成
        final var sb = new StringBuilder("https://livlog.xyz/asayuubiyori/api/sceneryJudgment");
        sb.append("?year=" + year);// 年を指定
        sb.append("&month=" + month);// 月を指定
        sb.append("&day=" + day);// 日を指定
        sb.append("&lat=" + item.access.get(0).lat);// 緯度を指定
        sb.append("&lng=" + item.access.get(0).lng); // 経度を指定
        sb.append("&height=0"); // 建物高さを指定

        // URLを保持する適切な値オブジェクトを作成する
        final var url = new URL(sb.toString());

        // URLで接続を開き、コネクションを取得する
        final var connection = (HttpURLConnection) url.openConnection();

        // インプットストリームを取得する
        try (final var reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)) {

            final var mapper = new ObjectMapper();
            final var model = mapper.readValue(reader, SceneryJudgmentModel.class);

            return model.results.get(0);
        }
    }
```

## 3. CSVに出力する
次に解析したデータをCSVで出力します。

OpenCSVのライブラリを使用して出力をします。
こちらの記事を参考にしています。

https://qiita.com/T-H9703EnAc/items/0cbb28935ca7a7e30d9c


```java
    private void writeCSV(final List <FirstSunriseHotelCsv> beanList, final Class <FirstSunriseHotelCsv> bean, final String path) throws Exception {

        final var file = new File(path);
        try (var csvWriter = new CSVWriter(new FileWriter(file))) {
            final var mappingStrategy = new CustomMappingStrategy <FirstSunriseHotelCsv>();
            mappingStrategy.setType(bean);
            final var beanToCsv = new StatefulBeanToCsvBuilder <FirstSunriseHotelCsv>(csvWriter)
                    .withSeparator(ICSVWriter.DEFAULT_SEPARATOR)
                    .withMappingStrategy(mappingStrategy)
                    .build();
            beanToCsv.write(beanList);
        } catch (final Exception e) {
            e.printStackTrace();
            throw new Exception();
        }
    }
```

```java
package jp.livlog.firstSunriseHotel.common;

import com.opencsv.bean.BeanField;
import com.opencsv.bean.ColumnPositionMappingStrategy;
import com.opencsv.bean.CsvBindByName;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;

import org.apache.commons.lang3.StringUtils;

public class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T>{
    @Override
    public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
        final int numColumns = getFieldMap().values().size();
        super.generateHeader(bean);

        String[] header = new String[numColumns];

        BeanField beanField;
        for (int i = 0; i < numColumns; i++) {
            beanField = findField(i);
            String columnHeaderName = extractHeaderName(beanField);
            header[i] = columnHeaderName;
        }
        return header;
    }

    private String extractHeaderName(final BeanField beanField) {
        if (beanField == null || beanField.getField() == null || beanField.getField().getDeclaredAnnotationsByType(
                CsvBindByName.class).length == 0) {
            return StringUtils.EMPTY;
        }

        final CsvBindByName bindByNameAnnotation = beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class)[0];
        return bindByNameAnnotation.column();
    }
}
```

```java
package jp.livlog.firstSunriseHotel.csv;

import com.opencsv.bean.CsvBindByName;
import com.opencsv.bean.CsvBindByPosition;

import lombok.Data;

@Data
public class FirstSunriseHotelCsv {

    @CsvBindByPosition (position = 0)
    @CsvBindByName (column = "id", required = true)
    private String id;

    @CsvBindByPosition (position = 1)
    @CsvBindByName (column = "name", required = true)
    private String name;

    @CsvBindByPosition (position = 2)
    @CsvBindByName (column = "address", required = true)
    private String address;

    @CsvBindByPosition (position = 3)
    @CsvBindByName (column = "lat", required = true)
    private double lat;

    @CsvBindByPosition (position = 4)
    @CsvBindByName (column = "lng", required = true)
    private double lng;

    @CsvBindByPosition (position = 5)
    @CsvBindByName (column = "sunriseRate", required = true)
    private double sunriseRate;
}
```

そして、全体の呼び出し部分
```java
    public static void main(final String[] args) {

        final var latlng = "34.6202363,138.8274184";
        final var limit = "100";
        final var apiKey = "<<----API KEY---->>";
        final var path = "C:\\temp\\FirstSunriseHotel.csv";

        try {

            final List <FirstSunriseHotelCsv> csvList = new ArrayList <>();

            final var firstSunriseHotel = new FirstSunriseHotel();
            final var items = firstSunriseHotel.getHotel(latlng, limit, apiKey);

            FirstSunriseHotelCsv csv = null;
            for (final Item item : items) {
                System.out.println(item);
                final var sceneryJudgment = firstSunriseHotel.getSceneryJudgment(item);
                if (sceneryJudgment != null) {
                    System.out.println(sceneryJudgment.sunriseRate);
                    csv = new FirstSunriseHotelCsv();
                    csv.setId(item.id);
                    csv.setName(item.title);
                    csv.setAddress(item.address.label);
                    csv.setLat(item.position.lat);
                    csv.setLng(item.position.lng);
                    csv.setSunriseRate(sceneryJudgment.sunriseRate);
                    csvList.add(csv);
                }

            }

            firstSunriseHotel.writeCSV(csvList, FirstSunriseHotelCsv.class, path);

        } catch (final Exception e) {
            e.printStackTrace();
        }
    }
```

出力されたCSVは、[【HERE Studio】](https://jp.developer.here.com/documentation/platform_studio/user_guide/index.html)に取り込んで、結果を表示していますが、[QGIS](https://www.qgis.org/ja/site/)やJavascriptの[leaflet](https://leafletjs.com/)などで、表示をしてもよいかと思います。

[【HERE Geocoding & Search API】](https://jp.developer.here.com/documentation/geocoding-search-api/dev_guide/index.html) で取得できる件数は最大で100件なので、泊まってみたいエリアを中心に緯度経度を指定すると穴場な初日の出の見えるホテルをみつけることができるかもしれませんね。

# 最後に
今回、作成したプログラムはGitHubで公開していますので、是非気になる場所で、初日の出を見たいホテルを探している方はこちらのプログラムを実行して初日の出見えるホテルを探してみてはどうでしょうか？

https://github.com/blue-islands/first-sunrise-hotel

実際に【HERE Geocoding & Search API】をさわってみて、ドキュメントは日本語になっていましたが、サンプルのAPIのパラメータが英語になっており、日本語はどのように動かせるのか？と試してみた部分はありましたが、ある程度は直感的に動かせたので、周辺のホテル一覧は簡単に取得することはできました。

自分の作成した【朝夕日和 API】と連携して、ホテルから見える初日の出として判定を行いましたが、ほかにもいろいろなシチュエーションで日時を変えて判定を行うとまた違った気持ちのよい場所が見つけれるかもしれません。

最後に朝夕日和のアプリを載せておきます。

https://asayuubiyori.hoshi-miru.com/

こちらを利用して、最高の朝日や夕日に出会えますように。。。