歡迎來到 GIS 的世界-Day 29:旅遊規劃小專案查景點和住宿
最後一個想要建立的功能,是可以在網頁上搜尋景點附近的住宿位置。
- 身為一個使用者,我可以點選景點開啟詳細資訊,並查詢不同公尺範圍內的住宿。
- 身為一個使用者,我可以在地圖上看到我所查詢的住宿位置。
API
針對第一點,後端要做的事情有:
- 找出景點的資料
- 建立一個 Buffer
- 和住宿的清單做比對
這裡要注意的地方是,Buffer 的建立在 TWD97 的座標系統底下才會是以公尺為單位,所以在建立 Buffer 和比對清單之前,要記得把景點和住宿的座標系統都換成 TWD97 喔。
[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 的部分只需要另外加上一顆按鈕就可以了:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>旅遊規劃</title>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"
integrity="sha512-hoalWLoI8r4UszCkZ5kL8vayOGVae1oxXe/2A4AO6J9+580uKHDO3JdHb7NzwwzK5xr/Fs0W40kiNHxM9vyTtQ=="
crossorigin=""/>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/MarkerCluster.Default.css">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/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/[email protected]/dist/leaflet.js"
integrity="sha512-BB3hKbKWOc9Ez/TAwyWxNXeoV9c1v6FIeYiBieIWkpLjauysF18NzgR1MBNBXf8/KABdlkX68nAhlwcDFLGPCQ=="
crossorigin=""></script>
<script src="https://unpkg.com/[email protected]/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/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
</body>
</html>查詢介面
至於 JavaScript 的部分,我們一樣利用 Sweet Alert 製作選單,只不過這次是兩層的,第一層是一個下拉式選單,可以選擇要查詢的景點:

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

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