chienweichang commited on
Commit
50c0328
1 Parent(s): c107815

Upload 6 files

Browse files
Files changed (6) hide show
  1. Dockerfile +20 -0
  2. index.html +93 -0
  3. main.js +252 -0
  4. no-image-icon-23494.png +0 -0
  5. styles.css +118 -0
  6. worker.js +9 -0
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 使用 Node.js 官方鏡像作為基礎鏡像
2
+ FROM node:14
3
+
4
+ # 設置工作目錄
5
+ WORKDIR /app
6
+
7
+ # 複製 package.json 和 package-lock.json(如果有)
8
+ COPY package*.json ./
9
+
10
+ # 安裝依賴
11
+ RUN npm install
12
+
13
+ # 複製應用程序文件
14
+ COPY . .
15
+
16
+ # 暴露應用運行的端口
17
+ EXPOSE 8080
18
+
19
+ # 運行應用程序
20
+ CMD ["npx", "http-server", "-p", "8080"]
index.html ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-TW">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>POI Map</title>
7
+ <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
8
+ <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
9
+ <link rel="stylesheet" href="styles.css" />
10
+ </head>
11
+ <body>
12
+ <div id="map"></div>
13
+ <div id="controls" class="container-fluid">
14
+ <div class="row mb-2">
15
+ <div class="col-md-4 col-12">
16
+ <div class="form-group">
17
+ <label for="poiTypeSelect">選擇POI類型</label>
18
+ <select id="poiTypeSelect" class="form-control">
19
+ <option value="all">All</option>
20
+ <option value="mrt">MRT</option>
21
+ <option value="school">SCHOOL</option>
22
+ <option value="landfill">LANDFILL</option>
23
+ <option value="hospital">HOSPITAL</option>
24
+ <option value="collage">COLLAGE</option>
25
+ <option value="park">PARK</option>
26
+ <option value="financial_industry">FINANCIAL_INDUSTRY</option>
27
+ <option value="entertainment">ENTERTAINMENT</option>
28
+ <option value="shopping">SHOPPING</option>
29
+ <option value="temple">TEMPLE</option>
30
+ <option value="funeral_industry">FUNERAL_INDUSTRY</option>
31
+ <option value="night_market">NIGHT_MARKET</option>
32
+ <option value="hsr">HSR</option>
33
+ <option value="gas_station">GAS_STATION</option>
34
+ <option value="station">STATION</option>
35
+ <option value="bus_station">BUS_STATION</option>
36
+ </select>
37
+ </div>
38
+ </div>
39
+ <div class="col-md-4 col-6">
40
+ <div class="form-group">
41
+ <label for="latInput">輸入緯度</label>
42
+ <input type="text" id="latInput" class="form-control" placeholder="輸入緯度" />
43
+ </div>
44
+ </div>
45
+ <div class="col-md-4 col-6">
46
+ <div class="form-group">
47
+ <label for="lngInput">輸入經度</label>
48
+ <input type="text" id="lngInput" class="form-control" placeholder="輸入經度" />
49
+ </div>
50
+ </div>
51
+ </div>
52
+ <div class="row mb-2">
53
+ <div class="col-12">
54
+ <button id="searchButton" class="btn btn-primary">查詢最近的前10個POI</button>
55
+ <button id="uploadButton" class="btn btn-secondary">上傳POI數據</button>
56
+ <input type="file" id="fileInput" style="display:none">
57
+ <button id="clearButton" class="btn btn-danger">清除POI</button>
58
+ </div>
59
+ </div>
60
+ <div class="row">
61
+ <div class="col-12 d-md-block">
62
+ <div id="info" class="mt-3"></div>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ <div id="legend" class="legend"></div>
67
+
68
+ <!-- 確認/取消通知模態框 -->
69
+ <div class="modal fade" id="confirmationModal" tabindex="-1" role="dialog" aria-labelledby="confirmationModalLabel" aria-hidden="true">
70
+ <div class="modal-dialog" role="document">
71
+ <div class="modal-content">
72
+ <div class="modal-header">
73
+ <h5 class="modal-title" id="confirmationModalLabel">確認操作</h5>
74
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
75
+ <span aria-hidden="true">&times;</span>
76
+ </button>
77
+ </div>
78
+ <div class="modal-body" id="confirmationMessage"></div>
79
+ <div class="modal-footer">
80
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
81
+ <button type="button" class="btn btn-primary" id="confirmButton">確認</button>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+
87
+ <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
88
+ <script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js"></script>
89
+ <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
90
+ <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
91
+ <script src="main.js"></script>
92
+ </body>
93
+ </html>
main.js ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 初始化地圖
2
+ var map = L.map('map').setView([23.5, 121], 8);
3
+ console.log('地圖初始化成功'); // 調試代碼
4
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
5
+ maxZoom: 18,
6
+ errorTileUrl: 'no-image-icon-23494.png' // 可選:設置錯誤時的替代圖像
7
+ }).addTo(map).on('tileerror', function(error) {
8
+ console.error('地圖圖層加載失敗', error); // 調試代碼
9
+ });
10
+ console.log('圖層添加成功'); // 調試代碼
11
+
12
+ // 測試地圖圖層加載
13
+ fetch('https://a.tile.openstreetmap.org/0/0/0.png')
14
+ .then(response => {
15
+ if (!response.ok) {
16
+ throw new Error('Network response was not ok');
17
+ }
18
+ return response.blob();
19
+ })
20
+ .then(blob => {
21
+ console.log('地圖圖層加載成功');
22
+ })
23
+ .catch(error => {
24
+ console.error('地圖圖層加載失敗', error);
25
+ });
26
+
27
+ // 定義顏色對應的poi_type
28
+ var poiTypeColors = {
29
+ "mrt": "red",
30
+ "school": "blue",
31
+ "landfill": "green",
32
+ "hospital": "yellow",
33
+ "collage": "purple",
34
+ "park": "orange",
35
+ "financial_industry": "cyan",
36
+ "entertainment": "magenta",
37
+ "shopping": "lime",
38
+ "temple": "brown",
39
+ "funeral_industry": "navy",
40
+ "night_market": "olive",
41
+ "hsr": "teal",
42
+ "gas_station": "pink",
43
+ "station": "coral",
44
+ "bus_station": "gold"
45
+ };
46
+
47
+ // 創建標記圖層
48
+ var markersLayer = L.layerGroup().addTo(map);
49
+ var poiMarkers = {};
50
+ var selectedMarker = null; // 用於保存所選位置的標記
51
+
52
+ // 動態生成下拉選單選項
53
+ var select = document.getElementById('poiTypeSelect');
54
+ var latInput = document.getElementById('latInput');
55
+ var lngInput = document.getElementById('lngInput');
56
+ var searchButton = document.getElementById('searchButton');
57
+ var uploadButton = document.getElementById('uploadButton');
58
+ var clearButton = document.getElementById('clearButton');
59
+ var fileInput = document.getElementById('fileInput');
60
+ var confirmationModal = $('#confirmationModal');
61
+ var confirmationMessage = document.getElementById('confirmationMessage');
62
+ var confirmButton = document.getElementById('confirmButton');
63
+
64
+ // 監聽POI類型選擇改變事件
65
+ select.addEventListener('change', function(e) {
66
+ var selectedType = e.target.value;
67
+ // 使用者選擇POI類型後,自動取得當前地圖中心點的經緯度進行查詢
68
+ var center = map.getCenter();
69
+ searchNearestPOIs(center.lat, center.lng, selectedType);
70
+ });
71
+
72
+ searchButton.addEventListener('click', function() {
73
+ var lat = parseFloat(latInput.value);
74
+ var lng = parseFloat(lngInput.value);
75
+ var poiType = select.value;
76
+
77
+ if (!isNaN(lat) && !isNaN(lng)) {
78
+ setMarker(lat, lng);
79
+ searchNearestPOIs(lat, lng, poiType);
80
+ } else {
81
+ alert('請輸入有效的經緯度');
82
+ }
83
+ });
84
+
85
+ // 上傳POI數據
86
+ uploadButton.addEventListener('click', function() {
87
+ fileInput.click();
88
+ });
89
+
90
+ fileInput.addEventListener('change', function() {
91
+ var file = fileInput.files[0];
92
+ if (file) {
93
+ confirmationMessage.textContent = '確定要上傳這個POI數據文件嗎?';
94
+ confirmButton.onclick = function() {
95
+ var formData = new FormData();
96
+ formData.append("file", file);
97
+
98
+ fetch('https://chienweichang-poi-data.hf.space/upload-poi', {
99
+ method: 'POST',
100
+ body: formData
101
+ })
102
+ .then(response => response.json())
103
+ .then(data => {
104
+ confirmationModal.modal('hide');
105
+ })
106
+ .catch(error => {
107
+ console.error('Error uploading POI data:', error);
108
+ confirmationModal.modal('hide');
109
+ alert('上傳POI數據失敗');
110
+ });
111
+ };
112
+ confirmationModal.modal('show');
113
+ }
114
+ });
115
+
116
+ // 清除POI
117
+ clearButton.addEventListener('click', function() {
118
+ confirmationMessage.textContent = '確定要清除所有POI數據嗎?';
119
+ confirmButton.onclick = function() {
120
+ fetch('https://chienweichang-poi-data.hf.space/clear-kdtrees', {
121
+ method: 'POST'
122
+ })
123
+ .then(response => response.json())
124
+ .then(data => {
125
+ confirmationModal.modal('hide');
126
+ })
127
+ .catch(error => {
128
+ console.error('Error clearing POI:', error);
129
+ confirmationModal.modal('hide');
130
+ alert('清除POI失敗');
131
+ });
132
+ };
133
+ confirmationModal.modal('show');
134
+ });
135
+
136
+ // 查詢最近的POI
137
+ function searchNearestPOIs(lat, lng, poiType) {
138
+ // 清空現有標記
139
+ markersLayer.clearLayers();
140
+ poiMarkers = {};
141
+
142
+ // 調用後端API獲取最近的POI
143
+ fetch(`https://chienweichang-poi-data.hf.space/poi/nearest?lat=${lat}&lng=${lng}&poi_type=${poiType}`)
144
+ .then(response => response.json())
145
+ .then(data => {
146
+ if (data && data.length > 0) {
147
+ let infoHtml = generatePOITable(data);
148
+ document.getElementById('info').innerHTML = infoHtml;
149
+
150
+ // 更新地圖圖例
151
+ updateLegend(new Set(data.map(poi => poi.poi_type)));
152
+
153
+ // 為每個表格行添加點擊事件
154
+ data.forEach((poi, index) => {
155
+ document.getElementById(`poi-${index}`).addEventListener('click', () => {
156
+ var marker = poiMarkers[index];
157
+ map.setView(marker.getLatLng(), 15); // 跳轉到POI位置並放大地圖
158
+ marker.openPopup(); // 開啟POI的彈出訊息
159
+
160
+ // 取消之前選中的標記樣式
161
+ Object.values(poiMarkers).forEach(m => m.getElement().classList.remove('highlight'));
162
+ // 添加選中標記的樣式
163
+ marker.getElement().classList.add('highlight');
164
+ });
165
+ });
166
+ } else {
167
+ document.getElementById('info').innerHTML = `沒有找到附近的 POI`;
168
+ }
169
+ })
170
+ .catch(error => {
171
+ console.error('Error fetching nearest POI:', error);
172
+ document.getElementById('info').innerHTML = `錯誤:無法獲取最近的POI`;
173
+ });
174
+ }
175
+
176
+ function generatePOITable(data) {
177
+ let infoHtml = `<table class="table table-bordered">
178
+ <thead>
179
+ <tr>
180
+ <th scope="col">名稱</th>
181
+ <th scope="col">類型</th>
182
+ <th scope="col">距離 (米)</th>
183
+ </tr>
184
+ </thead>
185
+ <tbody>`;
186
+
187
+ data.forEach((poi, index) => {
188
+ var customIcon = L.divIcon({
189
+ className: 'custom-icon',
190
+ html: `<div style="background-color: ${poiTypeColors[poi.poi_type] || 'gray'}; width: 12px; height: 12px;"></div>`
191
+ });
192
+
193
+ var marker = L.marker([poi.latitude, poi.longitude], { icon: customIcon })
194
+ .bindPopup(`<b>${poi.name}</b><br>距離:${poi.distance} 米`)
195
+ .bindTooltip(`<b>${poi.name}</b><br>距離:${poi.distance} 米`, { permanent: false, direction: 'top' })
196
+ .addTo(markersLayer);
197
+
198
+ // 保存marker以便後續使用
199
+ poiMarkers[index] = marker;
200
+
201
+ infoHtml += `<tr id="poi-${index}" class="poi-row">
202
+ <td>${poi.name}</td>
203
+ <td>${poi.poi_type}</td>
204
+ <td>${poi.distance}</td>
205
+ </tr>`;
206
+ });
207
+
208
+ infoHtml += `</tbody></table>`;
209
+ return infoHtml;
210
+ }
211
+
212
+ // 點擊地圖事件
213
+ map.on('click', function(e) {
214
+ var lat = e.latlng.lat;
215
+ var lng = e.latlng.lng;
216
+ var poiType = select.value;
217
+
218
+ latInput.value = lat.toFixed(6);
219
+ lngInput.value = lng.toFixed(6);
220
+
221
+ setMarker(lat, lng);
222
+ searchNearestPOIs(lat, lng, poiType);
223
+ });
224
+
225
+ // 在地圖上設置或更新標記
226
+ function setMarker(lat, lng) {
227
+ if (selectedMarker) {
228
+ map.removeLayer(selectedMarker);
229
+ }
230
+ selectedMarker = L.marker([lat, lng], {
231
+ icon: L.icon({
232
+ iconUrl: 'https://unpkg.com/[email protected]/dist/images/marker-icon.png',
233
+ shadowUrl: 'https://unpkg.com/[email protected]/dist/images/marker-shadow.png',
234
+ iconAnchor: [12, 41], // 設置圖標的錨點為圖標底部中心
235
+ shadowAnchor: [13, 41], // 設置陰影的錨點為陰影底部中心
236
+ popupAnchor: [0, -41] // 設置彈出框的錨點為圖標頂部
237
+ })
238
+ }).addTo(map);
239
+ map.setView([lat, lng], 15); // 跳轉到所選位置並放大地圖
240
+ }
241
+
242
+ // 更新地圖圖例
243
+ function updateLegend(displayedTypes) {
244
+ var legendHtml = '<b>圖例</b><br>';
245
+ displayedTypes.forEach(type => {
246
+ legendHtml += `<i style="background:${poiTypeColors[type]}; width: 12px; height: 12px; display: inline-block; margin-right: 5px;"></i> ${type}<br>`;
247
+ });
248
+ document.getElementById('legend').innerHTML = legendHtml;
249
+ }
250
+
251
+ // 初始加載圖例
252
+ updateLegend(new Set());
no-image-icon-23494.png ADDED
styles.css ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ display: flex;
3
+ margin: 0;
4
+ padding: 0;
5
+ height: 100vh;
6
+ flex-direction: column;
7
+ }
8
+
9
+ #map {
10
+ flex: 1;
11
+ height: 60vh;
12
+ position: relative;
13
+ }
14
+
15
+ #controls {
16
+ padding: 20px;
17
+ box-sizing: border-box;
18
+ }
19
+
20
+ #info, #modalInfo {
21
+ overflow-y: auto;
22
+ max-height: 40vh; /* 通用最大高度 */
23
+ padding: 10px;
24
+ font-size: 16px;
25
+ border: 1px solid #ccc;
26
+ background-color: #f9f9f9;
27
+ }
28
+
29
+ /* 桌面版表格高度 */
30
+ @media (min-width: 769px) {
31
+ #info {
32
+ max-height: 20vh; /* 桌面版最大高度 */
33
+ }
34
+ }
35
+
36
+ .legend {
37
+ background: white;
38
+ padding: 10px;
39
+ border-radius: 5px;
40
+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
41
+ line-height: 1.5em;
42
+ color: #555;
43
+ position: absolute;
44
+ bottom: 20px;
45
+ right: 20px;
46
+ z-index: 1000;
47
+ pointer-events: none;
48
+ }
49
+
50
+ @media (max-width: 768px) {
51
+ #map {
52
+ height: 50vh;
53
+ }
54
+
55
+ #searchButton {
56
+ display: none;
57
+ }
58
+
59
+ #showTableButton {
60
+ display: none; /* 隱藏顯示表格按鈕 */
61
+ }
62
+
63
+ #info {
64
+ display: block; /* 顯示表格 */
65
+ max-height: 30vh; /* 限制表格高度 */
66
+ }
67
+
68
+ #latInput, #lngInput, label[for="latInput"], label[for="lngInput"] {
69
+ display: none;
70
+ }
71
+
72
+ #legend {
73
+ display: none; /* 隱藏右下角圖例 */
74
+ }
75
+ }
76
+
77
+ @keyframes highlight {
78
+ 0% { transform: scale(1); }
79
+ 50% { transform: scale(1.5); }
80
+ 100% { transform: scale(1); }
81
+ }
82
+
83
+ #info table, #modalInfo table {
84
+ width: 100%;
85
+ margin-top: 10px;
86
+ }
87
+
88
+ #info th, #info td, #modalInfo th, #modalInfo td {
89
+ text-align: left;
90
+ padding: 8px;
91
+ }
92
+
93
+ #info th, #modalInfo th {
94
+ background-color: #f2f2f2;
95
+ }
96
+
97
+ .poi-row {
98
+ cursor: pointer;
99
+ }
100
+
101
+ .poi-row:hover {
102
+ background-color: #e0e0e0;
103
+ }
104
+
105
+ .poi-row:active {
106
+ background-color: #b0b0b0;
107
+ }
108
+
109
+ .custom-icon div {
110
+ display: inline-block;
111
+ border: 1px solid #555;
112
+ box-shadow: 0 0 2px rgba(0,0,0,0.5);
113
+ }
114
+
115
+ .custom-icon .highlight {
116
+ animation: highlight 0.6s ease-out;
117
+ border: 2px solid yellow;
118
+ }
worker.js ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ self.addEventListener('message', function(e) {
2
+ var data = e.data;
3
+ if (data.type === 'filter') {
4
+ var filteredData = data.allData.filter(function(d) {
5
+ return data.modelType === 'all' || d.model_type === data.modelType;
6
+ });
7
+ self.postMessage({type: 'filtered', data: filteredData});
8
+ }
9
+ });