> ## Content Index
> Fetch the complete content index at: https://blog.anthea.day/llms.txt
> Use this file to discover other available public pages before exploring further.

# 歡迎來到 GIS 的世界－Day 29：旅遊規劃小專案查景點和住宿
- URL: https://blog.anthea.day/series-2022-ithome-29/
- Published: 2022-10-12T16:41:25.000Z
- Updated: 2026-11-06T19:53:28.000Z
- Author: Anthea / 芫荽
- Tags: #Migrated-1792241843742, #Import 2026-10-17 12:57, Coding, 2022 iThome 鐵人賽：歡迎來到 GIS 的世界

最後一個想要建立的功能，是可以在網頁上搜尋景點附近的住宿位置。

> \- 身為一個使用者，我可以點選景點開啟詳細資訊，並查詢不同公尺範圍內的住宿。  
> \- 身為一個使用者，我可以在地圖上看到我所查詢的住宿位置。

### API

針對第一點，後端要做的事情有：

1. 找出景點的資料
2. 建立一個 Buffer
3. 和住宿的清單做比對

這裡要注意的地方是，Buffer 的建立在 TWD97 的座標系統底下才會是以公尺為單位，所以在建立 Buffer 和比對清單之前，要記得把景點和住宿的座標系統都換成 TWD97 喔。

```C#
[HttpGet("{meter:int}")]
public IActionResult SearchByScenicSpot(int meter, string scenicSpot)
{
    // 利用景點代碼找景點的資料
    var scenicSpot = _db.ScenicSpots.Find(scenicSpot);
    
    // 處理找不到景點的情況
    if (scenicSpot == null) throw new KeyNotFoundException($"找不到代碼為 {scenicSpotId} 的景點");
    
    // !!!
    // TWD97 座標系統底下的 Buffer 單位才是公尺
    var twd97Geom = ConvertFromWgs84ToTwd97(scenicSpot.Geom);
    var buffer = twd97Geom.Buffer(meter);
    
    var hotelInfos = _hotelRepo.Get()
        .Where(h => ConvertFromWgs84ToTwd97(h.Geom).Within(buffer))
        .Select(s => new HotelInfo()
        {
            Id = s.Id,
            Name = s.Name,
            Telephone = s.Telephone,
            Address = s.Address,
            X = s.Geom.Centroid.X,
            Y = s.Geom.Centroid.Y,
            Type = s.Type ?? "未分類",
            RoomCount = s.RoomCount,
            LowestPrice = s.LowestPrice,
            CeilingPrice = s.CeilingPrice,
            Email = s.Email,
            ParkingCount = s.ParkingCount
        }).ToList();
        
    var hotelsAroundScenicSpot = new HotelsAroundScenicSpot()
    {
        Infos = hotelInfos,
        ScenicSpot = new ScenicSpotInfo()
        {
            X = scenicSpot.X,
            Y = scenicSpot.Y,
        },
    };
    return Ok(hotelsAroundScenicSpot);
}

// 詳細情形可以參考第 16 天的內容
private Geometry ConvertFromWgs84ToTwd97(Geometry wgs84Geom)
{
    var wgs84Wkt = "GEOGCS[\"WGS84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0,AUTHORITY[\"EPSG\",\"8901\"]],UNIT[\"degree\",0.01745329251994328,AUTHORITY[\"EPSG\",\"9122\"]],AUTHORITY[\"EPSG\",\"4326\"]]";
    var twd97Wkt = "PROJCS[\"TWD97 / TM2 zone 121\",GEOGCS[\"TWD97\",DATUM[\"Taiwan_Datum_1997\",SPHEROID[\"GRS 1980\",6378137,298.257222101],TOWGS84[0,0,0,0,0,0,0]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433,AUTHORITY[\"EPSG\",\"9122\"]],AUTHORITY[\"EPSG\",\"3824\"]],PROJECTION[\"Transverse_Mercator\"],PARAMETER[\"latitude_of_origin\",0],PARAMETER[\"central_meridian\",121],PARAMETER[\"scale_factor\",0.9999],PARAMETER[\"false_easting\",250000],PARAMETER[\"false_northing\",0],UNIT[\"metre\",1],AXIS[\"Easting\",EAST],AXIS[\"Northing\",NORTH],AUTHORITY[\"EPSG\",\"3826\"]]";
    var coordinateSystemFactory = new CoordinateSystemFactory();
    var coordinateTransformationFactory =  new CoordinateTransformationFactory();
    var twd97 = coordinateSystemFactory.CreateFromWkt(Twd97Wkt);
    var wgs84 = coordinateSystemFactory.CreateFromWkt(Wgs84Wkt);
    var fromCoordinateSystems = coordinateTransformationFactory.CreateFromCoordinateSystems(wgs84, twd97);
    return Transform(wgs84Geom, fromCoordinateSystems.MathTransform).Centroid;
}
    
static Geometry Transform(Geometry geom, MathTransform transform)
{
    geom = geom.Copy();
    geom.Apply(new MTF(transform));
    return geom;
}

sealed class MTF : ICoordinateSequenceFilter
{
    private readonly MathTransform _mathTransform;
    public MTF(MathTransform mathTransform) => _mathTransform = mathTransform;
    public bool Done => false;
    public bool GeometryChanged => true;
    public void Filter(CoordinateSequence seq, int i)
    {
        double x = seq.GetX(i);
        double y = seq.GetY(i);
        _mathTransform.Transform(ref x, ref y);
        seq.SetX(i, x);
        seq.SetY(i, y);
    }
}
```

### 呈現住宿地點

HTML 的部分只需要另外加上一顆按鈕就可以了：

```HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>旅遊規劃</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.8.0/dist/leaflet.css"
          integrity="sha512-hoalWLoI8r4UszCkZ5kL8vayOGVae1oxXe/2A4AO6J9+580uKHDO3JdHb7NzwwzK5xr/Fs0W40kiNHxM9vyTtQ=="
          crossorigin=""/>
    <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.Default.css">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css">
    
    <style>
        body{
            height: 100vh;
        }
        
        #app, .row, #map{
            height: 100%;
        }
        
        #search-by-county{
            z-index: 1001;
            top: 1%;
            right: 0;
        }
        
        #search-hotel{
            z-index: 1001;
            top: 10%;
            right: 0;
        }
    </style>
</head>
<body>
<div id="app" class="container-fluid">
    <div class="row align-items-center position-relative">
        <div id="map"></div>
        <div id="search-by-county" class="position-absolute col-auto">
            <button type="button" class="btn btn-lg btn-primary"
                    @click.prevent="searchScenicByCounty">
                查詢
            </button>
        </div>
        <div id="search-hotel" class="position-absolute col-auto">
            <button type="button" class="btn btn-lg btn-primary" 
                    @click.prevent="showList">
                清單
            </button>
        </div>
    </div>
</div>
<script src="https://unpkg.com/leaflet@1.8.0/dist/leaflet.js"
        integrity="sha512-BB3hKbKWOc9Ez/TAwyWxNXeoV9c1v6FIeYiBieIWkpLjauysF18NzgR1MBNBXf8/KABdlkX68nAhlwcDFLGPCQ=="
        crossorigin=""></script>
<script src="https://unpkg.com/leaflet.markercluster@1.4.1/dist/leaflet.markercluster.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://unpkg.com/vue@3"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
</body>
      </html>
```

### 查詢介面

至於 JavaScript 的部分，我們一樣利用 Sweet Alert 製作選單，只不過這次是兩層的，第一層是一個下拉式選單，可以選擇要查詢的景點：

![](https://cdn-images-1.medium.com/max/800/0*THbJyT-32wGqrh-z.png)

第二層則是讓使用者輸入要查詢的範圍：

![](https://cdn-images-1.medium.com/max/800/0*WT98fLx7eqGyeDq5.png)

最後渲染出來的結果如下，粉紅色的房子是住宿，藍色的點則是原本的景點位置。

![](https://cdn-images-1.medium.com/max/800/0*EDokjG3dSkOKxmXM.png)

#### Reference

- [Leaflet 官方文件](https://leafletjs.com/reference.html?ref=blog.anthea.day)