Gordon Li commited on
Commit
117ae8e
·
1 Parent(s): b656af9
Files changed (4) hide show
  1. AirbnbMapVisualiser.py +115 -114
  2. TrafficSpot.py +57 -0
  3. app.py +75 -18
  4. style.css +184 -1
AirbnbMapVisualiser.py CHANGED
@@ -4,6 +4,7 @@ import folium
4
  from html import escape
5
  from transformers import AutoTokenizer
6
  import numpy as np
 
7
 
8
 
9
  class AirbnbMapVisualiser:
@@ -13,25 +14,103 @@ class AirbnbMapVisualiser:
13
  'password': '7033',
14
  'dsn': 'imz409.ust.hk:1521/imz409'
15
  }
16
- # Initialize the tokenizer
17
  self.tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased')
 
 
18
  try:
19
- self.all_listings = self.get_all_listings()
20
- if self.all_listings:
21
- self.neighborhoods = sorted(list(set(row[3] for row in self.all_listings if row[3])))
22
- else:
23
- self.neighborhoods = []
24
- print("No listings found in database")
25
  except Exception as e:
26
  print(f"Initialization error: {str(e)}")
27
- self.all_listings = []
28
  self.neighborhoods = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  def preprocess_text(self, text):
31
  if not isinstance(text, str):
32
  return ""
33
-
34
- # Tokenize and decode back to get normalized text
35
  tokens = self.tokenizer.tokenize(text)
36
  cleaned_text = " ".join(tokens)
37
  return cleaned_text
@@ -43,18 +122,14 @@ class AirbnbMapVisualiser:
43
  def compute_token_based_similarity(self, text1, text2):
44
  tokens1 = set(self.tokenizer.tokenize(text1.lower()))
45
  tokens2 = set(self.tokenizer.tokenize(text2.lower()))
46
-
47
  intersection = len(tokens1.intersection(tokens2))
48
-
49
  normalization_factor = min(len(tokens1), len(tokens2))
50
 
51
  if normalization_factor == 0:
52
  return 0
53
 
54
  base_similarity = intersection / normalization_factor
55
-
56
  boosted_score = 1 / (1 + np.exp(-6 * (base_similarity - 0.5)))
57
-
58
  return boosted_score
59
 
60
  def compute_search_scores(self, df, search_query):
@@ -68,21 +143,17 @@ class AirbnbMapVisualiser:
68
 
69
  token_sim = self.compute_token_based_similarity(processed_query, listing_desc)
70
 
71
- # Enhanced exact match bonus
72
  exact_match_bonus = 0
73
  partial_match_bonus = 0
74
 
75
  for term in query_terms:
76
- # Exact match bonus
77
  if term in desc_terms:
78
- exact_match_bonus += 0.2 # 20% bonus for each exact term match
79
 
80
- # Partial match bonus
81
  for desc_term in desc_terms:
82
  if term in desc_term or desc_term in term:
83
- partial_match_bonus += 0.1 # 10% bonus for partial matches
84
 
85
- # Calculate category match bonus
86
  category_keywords = {
87
  'location': ['near', 'close', 'located', 'area', 'district'],
88
  'amenities': ['wifi', 'kitchen', 'bathroom', 'furnished'],
@@ -94,18 +165,16 @@ class AirbnbMapVisualiser:
94
  for category, keywords in category_keywords.items():
95
  if any(keyword in processed_query.lower() for keyword in keywords):
96
  if any(keyword in listing_desc.lower() for keyword in keywords):
97
- category_bonus += 0.15 # 15% bonus for matching categories
98
 
99
- # Combine all scores with adjusted weights
100
  final_score = min(1.0, (
101
- token_sim * 0.4 + # Base similarity
102
- exact_match_bonus * 0.3 + # Exact matches
103
- partial_match_bonus * 0.2 + # Partial matches
104
- category_bonus * 0.1 # Category matches
105
  ))
106
 
107
- # Apply exponential boost to higher scores
108
- boosted_score = np.power(final_score, 0.7) # This will boost higher scores more
109
  scores.append(boosted_score)
110
 
111
  return np.array(scores)
@@ -114,33 +183,26 @@ class AirbnbMapVisualiser:
114
  if not search_query:
115
  return df
116
 
117
- # Get similarity scores
118
  scores = self.compute_search_scores(df, search_query)
119
-
120
- # Add scores to dataframe
121
  df['relevance_score'] = scores
122
-
123
- # Convert to percentage (0-100) with adjusted scaling
124
  df['relevance_percentage'] = df['relevance_score'] * 100
125
 
126
- # Adjust threshold levels for relevance descriptions
127
  def get_relevance_description(score):
128
- if score >= 70: # Lowered from 85
129
  return "Perfect match"
130
- elif score >= 55: # Lowered from 70
131
  return "Very relevant"
132
- elif score >= 40: # Lowered from 55
133
  return "Relevant"
134
- elif score >= 25: # Lowered from 40
135
  return "Somewhat relevant"
136
- elif score >= 15: # Lowered from 25
137
  return "Slightly relevant"
138
  else:
139
  return "Less relevant"
140
 
141
  df['relevance_features'] = df['relevance_percentage'].apply(get_relevance_description)
142
 
143
- # Enhanced matching features description
144
  def get_matching_features(row):
145
  query_tokens = set(self.tokenizer.tokenize(search_query.lower()))
146
  desc_tokens = set(self.tokenizer.tokenize(self.create_listing_description(row).lower()))
@@ -149,7 +211,6 @@ class AirbnbMapVisualiser:
149
  if not matching_tokens:
150
  return "No direct matches"
151
 
152
- # Group similar matches
153
  grouped_matches = []
154
  processed_tokens = set()
155
 
@@ -166,89 +227,48 @@ class AirbnbMapVisualiser:
166
  return ", ".join(grouped_matches)
167
 
168
  df['matching_features'] = df.apply(get_matching_features, axis=1)
169
-
170
- # Sort by relevance score (descending)
171
  return df.sort_values('relevance_score', ascending=False)
172
 
173
-
174
- def get_all_listings(self):
175
- try:
176
- with oracledb.connect(**self.connection_params) as conn:
177
- cursor = conn.cursor()
178
- cursor.execute("""
179
- SELECT ID, NAME, HOST_NAME, NEIGHBOURHOOD,
180
- LATITUDE, LONGITUDE, ROOM_TYPE, PRICE,
181
- NUMBER_OF_REVIEWS, REVIEWS_PER_MONTH,
182
- MINIMUM_NIGHTS, AVAILABILITY_365
183
- FROM airbnb_master_data
184
- WHERE LATITUDE IS NOT NULL
185
- AND LONGITUDE IS NOT NULL
186
- """)
187
- return cursor.fetchall()
188
- except Exception as e:
189
- print(f"Database error: {str(e)}")
190
- return []
191
-
192
- def get_traffic_spots(self):
193
- try:
194
- with oracledb.connect(**self.connection_params) as conn:
195
- cursor = conn.cursor()
196
- cursor.execute("""
197
- SELECT KEY, LATITUDE, LONGITUDE
198
- FROM TD_TRAFFIC_CAMERA_LOCATION
199
- WHERE LATITUDE IS NOT NULL
200
- AND LONGITUDE IS NOT NULL
201
- """)
202
- return cursor.fetchall()
203
- except Exception as e:
204
- print(f"Database error: {str(e)}")
205
- return []
206
-
207
  def create_map_and_data(self, neighborhood="Sha Tin", show_traffic=True, center_lat=None, center_lng=None,
208
  selected_id=None, search_query=None):
209
- if not self.all_listings:
 
 
210
  return None, None
211
 
212
- df = pd.DataFrame(self.all_listings, columns=[
213
  'id', 'name', 'host_name', 'neighbourhood',
214
  'latitude', 'longitude', 'room_type', 'price',
215
  'number_of_reviews', 'reviews_per_month',
216
  'minimum_nights', 'availability_365'
217
  ])
218
 
219
- # Filter by neighborhood
220
- df = df[df['neighbourhood'] == neighborhood].copy()
221
-
222
- # Convert numeric columns
223
  numeric_cols = ['latitude', 'longitude', 'price', 'number_of_reviews',
224
  'minimum_nights', 'availability_365', 'reviews_per_month']
225
  for col in numeric_cols:
226
  df[col] = pd.to_numeric(df[col], errors='coerce')
227
 
228
- # Apply search-based sorting if query exists
229
  if search_query:
230
  df = self.sort_by_relevance(df, search_query)
231
 
232
  if df.empty:
233
  return None, None
234
 
235
- # Calculate map center
236
  if center_lat is None or center_lng is None:
237
  center_lat = df['latitude'].mean()
238
  center_lng = df['longitude'].mean()
239
 
240
- # Create map
241
  m = folium.Map(
242
  location=[center_lat, center_lng],
243
  zoom_start=16 if (center_lat is not None and center_lng is not None) else 14,
244
  tiles='OpenStreetMap'
245
  )
246
 
247
- # Add markers for listings
248
  for idx, row in df.iterrows():
249
  marker_id = f"marker_{row['id']}"
 
 
250
 
251
- # Add relevance information to popup if search was performed
252
  relevance_info = ""
253
  if search_query and 'relevance_percentage' in row:
254
  relevance_info = f"""
@@ -267,6 +287,11 @@ class AirbnbMapVisualiser:
267
  <p style='margin: 5px 0;'><strong>Price:</strong> ${row['price']:.0f}</p>
268
  <p style='margin: 5px 0;'><strong>Reviews:</strong> {row['number_of_reviews']:.0f}</p>
269
  {relevance_info}
 
 
 
 
 
270
  </div>
271
  """
272
 
@@ -282,31 +307,7 @@ class AirbnbMapVisualiser:
282
 
283
  if selected_id is not None and row['id'] == selected_id:
284
  marker._name = marker_id
285
- m.get_root().html.add_child(folium.Element(f"""
286
- <script>
287
- document.addEventListener('DOMContentLoaded', function() {{
288
- setTimeout(function() {{
289
- document.querySelector('#{marker_id}').click();
290
- }}, 1000);
291
- }});
292
- </script>
293
- """))
294
-
295
- # Add traffic cameras if requested
296
- if show_traffic:
297
- traffic_spots = self.get_traffic_spots()
298
- for spot in traffic_spots:
299
- if spot[1] and spot[2]:
300
- popup_content = f"""
301
- <div style='min-width: 150px; padding: 10px;'>
302
- <p style='margin: 5px 0;'><strong>Traffic Camera ID:</strong> {escape(str(spot[0]))}</p>
303
- </div>
304
- """
305
-
306
- folium.Marker(
307
- location=[float(spot[1]), float(spot[2])],
308
- popup=popup_content,
309
- icon=folium.Icon(color='blue', icon='camera'),
310
- ).add_to(m)
311
 
 
 
312
  return m, df
 
4
  from html import escape
5
  from transformers import AutoTokenizer
6
  import numpy as np
7
+ from TrafficSpot import TrafficSpotManager
8
 
9
 
10
  class AirbnbMapVisualiser:
 
14
  'password': '7033',
15
  'dsn': 'imz409.ust.hk:1521/imz409'
16
  }
 
17
  self.tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased')
18
+ self.traffic_manager = TrafficSpotManager(self.connection_params)
19
+
20
  try:
21
+ # Get all neighborhoods first (this is a small query)
22
+ self.neighborhoods = self.get_all_neighborhoods()
23
+ # Initialize with Sha Tin listings
24
+ self.cached_listings = {}
25
+ self.cached_listings["Sha Tin"] = self.get_neighborhood_listings("Sha Tin")
 
26
  except Exception as e:
27
  print(f"Initialization error: {str(e)}")
 
28
  self.neighborhoods = []
29
+ self.cached_listings = {}
30
+
31
+ def get_all_neighborhoods(self):
32
+ try:
33
+ with oracledb.connect(**self.connection_params) as conn:
34
+ cursor = conn.cursor()
35
+ cursor.execute("""
36
+ SELECT DISTINCT NEIGHBOURHOOD
37
+ FROM airbnb_master_data
38
+ WHERE NEIGHBOURHOOD IS NOT NULL
39
+ ORDER BY NEIGHBOURHOOD
40
+ """)
41
+ neighborhoods = [row[0] for row in cursor.fetchall()]
42
+ return neighborhoods
43
+ except Exception as e:
44
+ print(f"Database error getting neighborhoods: {str(e)}")
45
+ return []
46
+
47
+ def get_neighborhood_listings(self, neighborhood):
48
+ # Check if listings are already cached
49
+ if neighborhood in self.cached_listings:
50
+ return self.cached_listings[neighborhood]
51
+
52
+ try:
53
+ with oracledb.connect(**self.connection_params) as conn:
54
+ cursor = conn.cursor()
55
+ cursor.execute("""
56
+ SELECT m.ID, m.NAME, m.HOST_NAME, m.NEIGHBOURHOOD,
57
+ m.LATITUDE, m.LONGITUDE, m.ROOM_TYPE, m.PRICE,
58
+ COUNT(r.LISTING_ID) as NUMBER_OF_REVIEWS, m.REVIEWS_PER_MONTH,
59
+ m.MINIMUM_NIGHTS, m.AVAILABILITY_365
60
+ FROM airbnb_master_data m
61
+ LEFT JOIN airbnb_reviews_data r ON m.ID = r.LISTING_ID
62
+ WHERE m.LATITUDE IS NOT NULL AND m.LONGITUDE IS NOT NULL AND NEIGHBOURHOOD = :neighborhood
63
+ GROUP BY m.ID, m.NAME, m.HOST_NAME, m.NEIGHBOURHOOD,
64
+ m.LATITUDE, m.LONGITUDE, m.ROOM_TYPE, m.PRICE,
65
+ m.REVIEWS_PER_MONTH, m.MINIMUM_NIGHTS, m.AVAILABILITY_365
66
+
67
+ """, neighborhood=neighborhood)
68
+ listings = cursor.fetchall()
69
+ # Cache the results
70
+ self.cached_listings[neighborhood] = listings
71
+ return listings
72
+ except Exception as e:
73
+ print(f"Database error: {str(e)}")
74
+ return []
75
+
76
+ def get_listing_reviews(self, listing_id):
77
+ try:
78
+ with oracledb.connect(**self.connection_params) as conn:
79
+ cursor = conn.cursor()
80
+ cursor.execute("""
81
+ SELECT REVIEW_DATE, REVIEWER_NAME,
82
+ CASE
83
+ WHEN LENGTH(COMMENTS) > 200
84
+ THEN SUBSTR(COMMENTS, 1, 200) || '...'
85
+ ELSE COMMENTS
86
+ END as COMMENTS
87
+ FROM AIRBNB_REVIEWS_DATA
88
+ WHERE LISTING_ID = :listing_id
89
+ ORDER BY REVIEW_DATE DESC
90
+ """, listing_id=str(listing_id))
91
+ reviews = cursor.fetchall()
92
+
93
+ formatted_reviews = []
94
+ for review in reviews:
95
+ review_date, reviewer_name, comments = review
96
+ formatted_review = (
97
+ str(review_date) if review_date else '',
98
+ str(reviewer_name) if reviewer_name else '',
99
+ str(comments) if comments else ''
100
+ )
101
+ formatted_reviews.append(formatted_review)
102
+
103
+ return formatted_reviews
104
+ except oracledb.DatabaseError as e:
105
+ print(f"Database error fetching reviews: {str(e)}")
106
+ return []
107
+ except Exception as e:
108
+ print(f"Error fetching reviews: {str(e)}")
109
+ return []
110
 
111
  def preprocess_text(self, text):
112
  if not isinstance(text, str):
113
  return ""
 
 
114
  tokens = self.tokenizer.tokenize(text)
115
  cleaned_text = " ".join(tokens)
116
  return cleaned_text
 
122
  def compute_token_based_similarity(self, text1, text2):
123
  tokens1 = set(self.tokenizer.tokenize(text1.lower()))
124
  tokens2 = set(self.tokenizer.tokenize(text2.lower()))
 
125
  intersection = len(tokens1.intersection(tokens2))
 
126
  normalization_factor = min(len(tokens1), len(tokens2))
127
 
128
  if normalization_factor == 0:
129
  return 0
130
 
131
  base_similarity = intersection / normalization_factor
 
132
  boosted_score = 1 / (1 + np.exp(-6 * (base_similarity - 0.5)))
 
133
  return boosted_score
134
 
135
  def compute_search_scores(self, df, search_query):
 
143
 
144
  token_sim = self.compute_token_based_similarity(processed_query, listing_desc)
145
 
 
146
  exact_match_bonus = 0
147
  partial_match_bonus = 0
148
 
149
  for term in query_terms:
 
150
  if term in desc_terms:
151
+ exact_match_bonus += 0.2
152
 
 
153
  for desc_term in desc_terms:
154
  if term in desc_term or desc_term in term:
155
+ partial_match_bonus += 0.1
156
 
 
157
  category_keywords = {
158
  'location': ['near', 'close', 'located', 'area', 'district'],
159
  'amenities': ['wifi', 'kitchen', 'bathroom', 'furnished'],
 
165
  for category, keywords in category_keywords.items():
166
  if any(keyword in processed_query.lower() for keyword in keywords):
167
  if any(keyword in listing_desc.lower() for keyword in keywords):
168
+ category_bonus += 0.15
169
 
 
170
  final_score = min(1.0, (
171
+ token_sim * 0.4 +
172
+ exact_match_bonus * 0.3 +
173
+ partial_match_bonus * 0.2 +
174
+ category_bonus * 0.1
175
  ))
176
 
177
+ boosted_score = np.power(final_score, 0.7)
 
178
  scores.append(boosted_score)
179
 
180
  return np.array(scores)
 
183
  if not search_query:
184
  return df
185
 
 
186
  scores = self.compute_search_scores(df, search_query)
 
 
187
  df['relevance_score'] = scores
 
 
188
  df['relevance_percentage'] = df['relevance_score'] * 100
189
 
 
190
  def get_relevance_description(score):
191
+ if score >= 70:
192
  return "Perfect match"
193
+ elif score >= 55:
194
  return "Very relevant"
195
+ elif score >= 40:
196
  return "Relevant"
197
+ elif score >= 25:
198
  return "Somewhat relevant"
199
+ elif score >= 15:
200
  return "Slightly relevant"
201
  else:
202
  return "Less relevant"
203
 
204
  df['relevance_features'] = df['relevance_percentage'].apply(get_relevance_description)
205
 
 
206
  def get_matching_features(row):
207
  query_tokens = set(self.tokenizer.tokenize(search_query.lower()))
208
  desc_tokens = set(self.tokenizer.tokenize(self.create_listing_description(row).lower()))
 
211
  if not matching_tokens:
212
  return "No direct matches"
213
 
 
214
  grouped_matches = []
215
  processed_tokens = set()
216
 
 
227
  return ", ".join(grouped_matches)
228
 
229
  df['matching_features'] = df.apply(get_matching_features, axis=1)
 
 
230
  return df.sort_values('relevance_score', ascending=False)
231
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  def create_map_and_data(self, neighborhood="Sha Tin", show_traffic=True, center_lat=None, center_lng=None,
233
  selected_id=None, search_query=None):
234
+ listings = self.get_neighborhood_listings(neighborhood)
235
+
236
+ if not listings:
237
  return None, None
238
 
239
+ df = pd.DataFrame(listings, columns=[
240
  'id', 'name', 'host_name', 'neighbourhood',
241
  'latitude', 'longitude', 'room_type', 'price',
242
  'number_of_reviews', 'reviews_per_month',
243
  'minimum_nights', 'availability_365'
244
  ])
245
 
 
 
 
 
246
  numeric_cols = ['latitude', 'longitude', 'price', 'number_of_reviews',
247
  'minimum_nights', 'availability_365', 'reviews_per_month']
248
  for col in numeric_cols:
249
  df[col] = pd.to_numeric(df[col], errors='coerce')
250
 
 
251
  if search_query:
252
  df = self.sort_by_relevance(df, search_query)
253
 
254
  if df.empty:
255
  return None, None
256
 
 
257
  if center_lat is None or center_lng is None:
258
  center_lat = df['latitude'].mean()
259
  center_lng = df['longitude'].mean()
260
 
 
261
  m = folium.Map(
262
  location=[center_lat, center_lng],
263
  zoom_start=16 if (center_lat is not None and center_lng is not None) else 14,
264
  tiles='OpenStreetMap'
265
  )
266
 
 
267
  for idx, row in df.iterrows():
268
  marker_id = f"marker_{row['id']}"
269
+ reviews = self.get_listing_reviews(row['id'])
270
+ review_button_key = f"review_btn_{row['id']}"
271
 
 
272
  relevance_info = ""
273
  if search_query and 'relevance_percentage' in row:
274
  relevance_info = f"""
 
287
  <p style='margin: 5px 0;'><strong>Price:</strong> ${row['price']:.0f}</p>
288
  <p style='margin: 5px 0;'><strong>Reviews:</strong> {row['number_of_reviews']:.0f}</p>
289
  {relevance_info}
290
+ <button onclick="streamlit_click('{review_button_key}')"
291
+ style="background-color: #4CAF50; color: white; padding: 8px 15px; border: none;
292
+ border-radius: 4px; cursor: pointer; margin-top: 10px;">
293
+ View Reviews ({len(reviews)})
294
+ </button>
295
  </div>
296
  """
297
 
 
307
 
308
  if selected_id is not None and row['id'] == selected_id:
309
  marker._name = marker_id
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
 
311
+ if show_traffic:
312
+ self.traffic_manager.add_spots_to_map(m)
313
  return m, df
TrafficSpot.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import oracledb
2
+ from html import escape
3
+ import folium
4
+
5
+ class TrafficSpot:
6
+ def __init__(self, key, latitude, longitude):
7
+ self.key = key
8
+ self.latitude = float(latitude) if latitude is not None else None
9
+ self.longitude = float(longitude) if longitude is not None else None
10
+
11
+ def is_valid(self):
12
+ return self.latitude is not None and self.longitude is not None
13
+
14
+ def create_popup_content(self):
15
+ return f"""
16
+ <div style='min-width: 150px; padding: 10px;'>
17
+ <p style='margin: 5px 0;'><strong>Traffic Camera ID:</strong> {escape(str(self.key))}</p>
18
+ </div>
19
+ """
20
+
21
+ def add_to_map(self, folium_map):
22
+ if self.is_valid():
23
+ folium.Marker(
24
+ location=[self.latitude, self.longitude],
25
+ popup=self.create_popup_content(),
26
+ icon=folium.Icon(color='blue', icon='camera'),
27
+ ).add_to(folium_map)
28
+
29
+
30
+ class TrafficSpotManager:
31
+ def __init__(self, connection_params):
32
+ self.connection_params = connection_params
33
+ self.traffic_spots = []
34
+ self.load_traffic_spots()
35
+
36
+ def load_traffic_spots(self):
37
+ try:
38
+ with oracledb.connect(**self.connection_params) as conn:
39
+ cursor = conn.cursor()
40
+ cursor.execute("""
41
+ SELECT KEY, LATITUDE, LONGITUDE
42
+ FROM TD_TRAFFIC_CAMERA_LOCATION
43
+ WHERE LATITUDE IS NOT NULL
44
+ AND LONGITUDE IS NOT NULL
45
+ """)
46
+ spots = cursor.fetchall()
47
+ self.traffic_spots = [
48
+ TrafficSpot(spot[0], spot[1], spot[2])
49
+ for spot in spots
50
+ ]
51
+ except Exception as e:
52
+ print(f"Database error loading traffic spots: {str(e)}")
53
+ self.traffic_spots = []
54
+
55
+ def add_spots_to_map(self, folium_map):
56
+ for spot in self.traffic_spots:
57
+ spot.add_to_map(folium_map)
app.py CHANGED
@@ -4,14 +4,40 @@ from streamlit_folium import st_folium, folium_static
4
  import math
5
  from AirbnbMapVisualiser import AirbnbMapVisualiser
6
 
 
7
  def load_css(css_file):
8
  with open(css_file) as f:
9
  st.markdown(f'<style>{f.read()}</style>', unsafe_allow_html=True)
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  def main():
12
  st.set_page_config(
13
  layout="wide",
14
- page_title="HK Airbnb & Traffic Camera Explorer",
15
  initial_sidebar_state="expanded"
16
  )
17
  load_css('style.css')
@@ -33,19 +59,31 @@ def main():
33
  st.session_state.search_query = ""
34
  if 'tokenizer_loaded' not in st.session_state:
35
  st.session_state.tokenizer_loaded = False
 
 
 
 
 
 
36
 
37
  # Initialize visualizer with loading message for tokenizer
38
- if not st.session_state.tokenizer_loaded:
39
  with st.spinner('Loading BERT tokenizer...'):
40
- visualizer = AirbnbMapVisualiser()
41
  st.session_state.tokenizer_loaded = True
42
- else:
43
- visualizer = AirbnbMapVisualiser()
 
 
 
 
 
44
 
45
  with st.sidebar:
46
- st.markdown('<p class="sidebar-header">HKUST AirBNB+ (External Hall Scheme for Non Local PG Students) <BR/></p>', unsafe_allow_html=True)
 
 
47
 
48
- # Enhanced search bar with placeholder
49
  search_query = st.text_input(
50
  "🔍 Search listings",
51
  value=st.session_state.search_query,
@@ -55,9 +93,9 @@ def main():
55
  if search_query != st.session_state.search_query:
56
  st.session_state.search_query = search_query
57
  st.session_state.current_page = 1
 
58
 
59
- st.markdown('<hr style="margin: 20px 0; border: none; border-top: 1px solid #e0e0e0;">',
60
- unsafe_allow_html=True)
61
 
62
  neighborhood = st.selectbox(
63
  "Select Neighborhood",
@@ -72,6 +110,7 @@ def main():
72
  st.session_state.selected_id = None
73
  st.session_state.current_page = 1
74
  st.session_state.search_query = ""
 
75
  st.rerun()
76
 
77
  # Create map and get data
@@ -92,6 +131,7 @@ def main():
92
  st.session_state.center_lat = df.iloc[0]['latitude']
93
  st.session_state.center_lng = df.iloc[0]['longitude']
94
  st.session_state.previous_neighborhood = neighborhood
 
95
  st.rerun()
96
 
97
  if m is None:
@@ -114,16 +154,13 @@ def main():
114
 
115
  st.markdown('<div class="scrollable-container">', unsafe_allow_html=True)
116
 
117
- # Enhanced listing display with matching features
118
  for idx in range(start_idx, end_idx):
119
  row = df.iloc[idx]
120
  background_color = "#E3F2FD" if st.session_state.selected_id == row['id'] else "white"
121
 
122
- # Enhanced relevance info with matching features
123
  relevance_info = ""
124
  if st.session_state.search_query and 'relevance_percentage' in row:
125
- relevance_info = f"""<p class="listing-info"> 🎯 Relevance: {row['relevance_percentage']:.0f}% </p>
126
- """
127
  if 'matching_features' in row:
128
  matching_features = row['matching_features']
129
  if matching_features and matching_features != "No direct matches":
@@ -138,11 +175,23 @@ def main():
138
  {relevance_info}</div>
139
  """, unsafe_allow_html=True)
140
 
141
- if st.button("View Details", key=f"btn_{row['id']}"):
142
- st.session_state.selected_id = row['id']
143
- st.session_state.center_lat = row['latitude']
144
- st.session_state.center_lng = row['longitude']
145
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
146
 
147
  st.markdown('</div>', unsafe_allow_html=True)
148
 
@@ -166,6 +215,7 @@ def main():
166
  st.session_state.selected_id = df.iloc[new_start_idx]['id']
167
  st.session_state.center_lat = df.iloc[new_start_idx]['latitude']
168
  st.session_state.center_lng = df.iloc[new_start_idx]['longitude']
 
169
  st.rerun()
170
 
171
  with col_prev:
@@ -176,6 +226,7 @@ def main():
176
  st.session_state.selected_id = df.iloc[new_start_idx]['id']
177
  st.session_state.center_lat = df.iloc[new_start_idx]['latitude']
178
  st.session_state.center_lng = df.iloc[new_start_idx]['longitude']
 
179
  st.rerun()
180
 
181
  with col_next:
@@ -186,7 +237,13 @@ def main():
186
  st.session_state.selected_id = df.iloc[new_start_idx]['id']
187
  st.session_state.center_lat = df.iloc[new_start_idx]['latitude']
188
  st.session_state.center_lng = df.iloc[new_start_idx]['longitude']
 
189
  st.rerun()
190
 
 
 
 
 
 
191
  if __name__ == "__main__":
192
  main()
 
4
  import math
5
  from AirbnbMapVisualiser import AirbnbMapVisualiser
6
 
7
+
8
  def load_css(css_file):
9
  with open(css_file) as f:
10
  st.markdown(f'<style>{f.read()}</style>', unsafe_allow_html=True)
11
 
12
+
13
+ def render_review_dialog():
14
+ # Display reviews
15
+ with st.container():
16
+ col_title = st.columns([5, 1])
17
+ with col_title[0]:
18
+ st.markdown(f"### Reviews for {st.session_state.current_review_listing_name}")
19
+ reviews = st.session_state.visualizer.get_listing_reviews(st.session_state.current_review_listing)
20
+ if reviews:
21
+ for review in reviews:
22
+ try:
23
+ review_date, reviewer_name, comments = review
24
+ st.markdown(f"""
25
+ <div class="review-card">
26
+ <div class="review-header">
27
+ {escape(str(reviewer_name))} - {escape(str(review_date))}
28
+ </div>
29
+ <div class="review-content">
30
+ {escape(str(comments))}
31
+ </div>
32
+ </div>""", unsafe_allow_html=True)
33
+ except Exception as e:
34
+ st.error(f"Error displaying review: {str(e)}")
35
+ else:
36
+ st.info("No reviews available for this listing.")
37
  def main():
38
  st.set_page_config(
39
  layout="wide",
40
+ page_title="HKUST AirBNB+",
41
  initial_sidebar_state="expanded"
42
  )
43
  load_css('style.css')
 
59
  st.session_state.search_query = ""
60
  if 'tokenizer_loaded' not in st.session_state:
61
  st.session_state.tokenizer_loaded = False
62
+ if 'show_review_dialog' not in st.session_state:
63
+ st.session_state.show_review_dialog = False
64
+ if 'current_review_listing' not in st.session_state:
65
+ st.session_state.current_review_listing = None
66
+ if 'current_review_listing_name' not in st.session_state:
67
+ st.session_state.current_review_listing_name = None
68
 
69
  # Initialize visualizer with loading message for tokenizer
70
+ if 'visualizer' not in st.session_state:
71
  with st.spinner('Loading BERT tokenizer...'):
72
+ st.session_state.visualizer = AirbnbMapVisualiser()
73
  st.session_state.tokenizer_loaded = True
74
+
75
+ visualizer = st.session_state.visualizer
76
+
77
+ # Check if visualizer is properly initialized
78
+ if visualizer is None or not hasattr(visualizer, 'neighborhoods'):
79
+ st.error("Error initializing the application. Please refresh the page.")
80
+ return
81
 
82
  with st.sidebar:
83
+ st.markdown(
84
+ '<p class="sidebar-header">HKUST AirBNB+ (External Hall Scheme for Non Local PG Students) <BR/></p>',
85
+ unsafe_allow_html=True)
86
 
 
87
  search_query = st.text_input(
88
  "🔍 Search listings",
89
  value=st.session_state.search_query,
 
93
  if search_query != st.session_state.search_query:
94
  st.session_state.search_query = search_query
95
  st.session_state.current_page = 1
96
+ st.session_state.show_review_dialog = False
97
 
98
+ st.markdown('<hr style="margin: 20px 0; border: none; border-top: 1px solid #e0e0e0;">', unsafe_allow_html=True)
 
99
 
100
  neighborhood = st.selectbox(
101
  "Select Neighborhood",
 
110
  st.session_state.selected_id = None
111
  st.session_state.current_page = 1
112
  st.session_state.search_query = ""
113
+ st.session_state.show_review_dialog = False
114
  st.rerun()
115
 
116
  # Create map and get data
 
131
  st.session_state.center_lat = df.iloc[0]['latitude']
132
  st.session_state.center_lng = df.iloc[0]['longitude']
133
  st.session_state.previous_neighborhood = neighborhood
134
+ st.session_state.show_review_dialog = False
135
  st.rerun()
136
 
137
  if m is None:
 
154
 
155
  st.markdown('<div class="scrollable-container">', unsafe_allow_html=True)
156
 
 
157
  for idx in range(start_idx, end_idx):
158
  row = df.iloc[idx]
159
  background_color = "#E3F2FD" if st.session_state.selected_id == row['id'] else "white"
160
 
 
161
  relevance_info = ""
162
  if st.session_state.search_query and 'relevance_percentage' in row:
163
+ relevance_info = f"""<p class="listing-info"> 🎯 Relevance: {row['relevance_percentage']:.0f}% </p>"""
 
164
  if 'matching_features' in row:
165
  matching_features = row['matching_features']
166
  if matching_features and matching_features != "No direct matches":
 
175
  {relevance_info}</div>
176
  """, unsafe_allow_html=True)
177
 
178
+ col_details, col_reviews = st.columns(2)
179
+
180
+ with col_details:
181
+ if st.button("View Details", key=f"btn_{row['id']}"):
182
+ st.session_state.selected_id = row['id']
183
+ st.session_state.center_lat = row['latitude']
184
+ st.session_state.center_lng = row['longitude']
185
+ st.rerun()
186
+
187
+ with col_reviews:
188
+ if st.button("View Reviews", key=f"review_btn_{row['id']}"):
189
+ st.session_state.show_review_dialog = True
190
+ st.session_state.current_review_listing = row['id']
191
+ st.session_state.current_review_listing_name = row['name']
192
+ st.session_state.scroll_to_review = True
193
+ st.rerun()
194
+
195
 
196
  st.markdown('</div>', unsafe_allow_html=True)
197
 
 
215
  st.session_state.selected_id = df.iloc[new_start_idx]['id']
216
  st.session_state.center_lat = df.iloc[new_start_idx]['latitude']
217
  st.session_state.center_lng = df.iloc[new_start_idx]['longitude']
218
+ st.session_state.show_review_dialog = False
219
  st.rerun()
220
 
221
  with col_prev:
 
226
  st.session_state.selected_id = df.iloc[new_start_idx]['id']
227
  st.session_state.center_lat = df.iloc[new_start_idx]['latitude']
228
  st.session_state.center_lng = df.iloc[new_start_idx]['longitude']
229
+ st.session_state.show_review_dialog = False
230
  st.rerun()
231
 
232
  with col_next:
 
237
  st.session_state.selected_id = df.iloc[new_start_idx]['id']
238
  st.session_state.center_lat = df.iloc[new_start_idx]['latitude']
239
  st.session_state.center_lng = df.iloc[new_start_idx]['longitude']
240
+ st.session_state.show_review_dialog = False
241
  st.rerun()
242
 
243
+ # Show review dialog if active
244
+ if st.session_state.show_review_dialog:
245
+ render_review_dialog()
246
+
247
+
248
  if __name__ == "__main__":
249
  main()
style.css CHANGED
@@ -227,4 +227,187 @@
227
  margin: 16px 0;
228
  border-radius: 4px;
229
  color: #C62828;
230
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  margin: 16px 0;
228
  border-radius: 4px;
229
  color: #C62828;
230
+ }
231
+
232
+ .review-card {
233
+ background: white;
234
+ border: 1px solid #e0e0e0;
235
+ border-radius: 8px;
236
+ padding: 16px;
237
+ margin-bottom: 16px;
238
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
239
+ }
240
+
241
+ .review-header {
242
+ display: flex;
243
+ justify-content: space-between;
244
+ align-items: center;
245
+ margin-bottom: 12px;
246
+ }
247
+
248
+ .reviewer-name {
249
+ font-weight: bold;
250
+ color: #1a73e8;
251
+ }
252
+
253
+ .review-date {
254
+ color: #666;
255
+ font-size: 0.9em;
256
+ }
257
+
258
+ .review-content {
259
+ line-height: 1.6;
260
+ color: #333;
261
+ }
262
+
263
+ .relevance-indicator {
264
+ font-size: 0.9em;
265
+ font-weight: 500;
266
+ padding: 4px 8px;
267
+ border-radius: 4px;
268
+ background: rgba(0,0,0,0.05);
269
+ display: inline-block;
270
+ }
271
+
272
+ .review-search-container {
273
+ margin-bottom: 20px;
274
+ }
275
+
276
+ .review-filters {
277
+ display: flex;
278
+ gap: 16px;
279
+ margin-bottom: 16px;
280
+ }
281
+
282
+ .scrollable-reviews {
283
+ max-height: 600px;
284
+ overflow-y: auto;
285
+ padding-right: 16px;
286
+ }
287
+
288
+ .listing-card {
289
+ padding: 15px;
290
+ margin-bottom: 15px;
291
+ border-radius: 8px;
292
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
293
+ }
294
+
295
+ .listing-title {
296
+ margin: 0 0 10px 0;
297
+ font-size: 1.1em;
298
+ font-weight: bold;
299
+ }
300
+
301
+ .listing-info {
302
+ margin: 5px 0;
303
+ font-size: 0.9em;
304
+ }
305
+
306
+ .sidebar-header {
307
+ font-size: 1.2em;
308
+ font-weight: bold;
309
+ margin-bottom: 20px;
310
+ }
311
+
312
+ .map-container {
313
+ border-radius: 10px;
314
+ overflow: hidden;
315
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
316
+ }
317
+
318
+ .scrollable-container {
319
+ max-height: calc(100vh - 100px);
320
+ overflow-y: auto;
321
+ padding-right: 10px;
322
+ }
323
+
324
+ .listing-card {
325
+ padding: 15px;
326
+ margin-bottom: 15px;
327
+ border-radius: 8px;
328
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
329
+ transition: background-color 0.3s ease;
330
+ }
331
+
332
+ .listing-title {
333
+ margin: 0 0 10px 0;
334
+ font-size: 1.1em;
335
+ font-weight: bold;
336
+ color: #2c3e50;
337
+ }
338
+
339
+ .listing-info {
340
+ margin: 5px 0;
341
+ font-size: 0.9em;
342
+ color: #34495e;
343
+ }
344
+
345
+ /* Dialog styling */
346
+ .dialog-content {
347
+ background-color: white;
348
+ padding: 20px;
349
+ border-radius: 10px;
350
+ box-shadow: 0 4px 20px rgba(0,0,0,0.15);
351
+ margin: 20px 0;
352
+ max-height: 80vh;
353
+ overflow-y: auto;
354
+ }
355
+
356
+ .review-card {
357
+ background-color: #f8f9fa;
358
+ border-left: 4px solid #4CAF50;
359
+ padding: 15px;
360
+ margin: 10px 0;
361
+ border-radius: 4px;
362
+ }
363
+
364
+ .review-header {
365
+ font-weight: bold;
366
+ color: #2c3e50;
367
+ margin-bottom: 8px;
368
+ }
369
+
370
+ .review-content {
371
+ color: #34495e;
372
+ line-height: 1.6;
373
+ font-size: 0.95em;
374
+ }
375
+
376
+ /* Button styling */
377
+ .stButton button {
378
+ width: 100%;
379
+ border-radius: 4px;
380
+ transition: all 0.3s ease;
381
+ }
382
+
383
+ .stButton button:hover {
384
+ transform: translateY(-1px);
385
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
386
+ }
387
+
388
+ /* Pagination styling */
389
+ .pagination {
390
+ display: flex;
391
+ justify-content: center;
392
+ align-items: center;
393
+ margin-top: 20px;
394
+ }
395
+
396
+ /* Custom scrollbar */
397
+ ::-webkit-scrollbar {
398
+ width: 8px;
399
+ }
400
+
401
+ ::-webkit-scrollbar-track {
402
+ background: #f1f1f1;
403
+ border-radius: 4px;
404
+ }
405
+
406
+ ::-webkit-scrollbar-thumb {
407
+ background: #888;
408
+ border-radius: 4px;
409
+ }
410
+
411
+ ::-webkit-scrollbar-thumb:hover {
412
+ background: #555;
413
+ }