donwanhk commited on
Commit
cd47d5c
·
1 Parent(s): fea7b9b

AI employed

Browse files
Files changed (3) hide show
  1. .gitignore +2 -0
  2. app.py +204 -16
  3. requirements.txt +3 -0
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ .cursor
2
+ .venv
app.py CHANGED
@@ -1,27 +1,215 @@
1
  import gradio as gr
 
 
 
 
 
2
 
3
- def letter_counter(word, letter):
4
- """
5
- Count the number of occurrences of a letter in a word or text.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  Args:
8
- word (str): The input text to search through
9
- letter (str): The letter to search for
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
- Returns:
12
- str: A message indicating how many times the letter appears
13
  """
14
- word = word.lower()
15
- letter = letter.lower()
16
- count = word.count(letter)
17
- return count
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
  demo = gr.Interface(
20
- fn=letter_counter,
21
- inputs=[gr.Textbox("strawberry"), gr.Textbox("r")],
22
- outputs=[gr.Number()],
23
- title="Letter Counter",
24
- description="Enter text and a letter to count how many times the letter appears in the text."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  )
26
 
27
  if __name__ == "__main__":
 
1
  import gradio as gr
2
+ from typing import Any, List, Dict, Optional
3
+ import httpx
4
+ import math
5
+ from pydantic import BaseModel, Field, ValidationError
6
+ import datetime
7
 
8
+ # Constants
9
+ DPO_API_BASE = "https://api.data.gov.hk"
10
+ USER_AGENT = "carpark-app/1.0"
11
+
12
+ class VehicleVacancy(BaseModel):
13
+ vacancy: Optional[int] = None
14
+ vacancyEV: Optional[int] = None
15
+ vacancyDIS: Optional[int] = None
16
+ lastupdate: Optional[str] = None
17
+
18
+ class CarPark(BaseModel):
19
+ park_Id: str
20
+ name: Optional[str] = None
21
+ displayAddress: Optional[str] = None
22
+ vacancy: Dict[str, List[VehicleVacancy]] = Field(default_factory=dict)
23
+
24
+ class CarParkApiResponse(BaseModel):
25
+ results: List[CarPark]
26
+
27
+ async def make_dpo_request(url: str) -> dict[str, Any] | None:
28
+ """Make a request to the DPO API with proper error handling."""
29
+ headers = {
30
+ "User-Agent": USER_AGENT,
31
+ "Accept": "application/json"
32
+ }
33
+ async with httpx.AsyncClient() as client:
34
+ try:
35
+ response = await client.get(url, headers=headers, timeout=30.0)
36
+ response.raise_for_status()
37
+ return response.json()
38
+ except Exception:
39
+ return None
40
+
41
+ async def get_carpark_vacancy(lat: str, lng: str, vehicle_type: str = 'privateCar') -> str:
42
+ """Get car park vacancy nearby a location.
43
 
44
  Args:
45
+ lat: Minimum latitude of the location
46
+ lng: Maximum latitude of the location
47
+ """
48
+ extent = get_bounding_box(float(lat), float(lng))
49
+ carparks = await get_carparks_by_extent_and_vehicle_type(extent, vehicle_type)
50
+ markdown_str = format_carparks_markdown(carparks)
51
+ return markdown_str
52
+
53
+ async def get_carparks_by_extent_and_vehicle_type(extent: tuple[float, float, float, float], vehicle_type: str) -> List[CarPark]:
54
+ """
55
+ Query car parks within a bounding box (extent) and for a specific vehicle type.
56
+ Returns a list of CarPark objects with validated data.
57
+ extent: (min_lng, min_lat, max_lng, max_lat)
58
+ vehicle_type: e.g. 'privateCar', 'LGV', etc.
59
+ """
60
+ min_lng, min_lat, max_lng, max_lat = extent
61
+ # 1. First call: get vacancies
62
+ vacancy_url = (
63
+ f"{DPO_API_BASE}/v1/carpark-info-vacancy?data=vacancy&lang=zh_TW"
64
+ f"&extent={min_lng},{min_lat},{max_lng},{max_lat}"
65
+ f"&vehicleTypes={vehicle_type}"
66
+ )
67
+ vacancy_data = await make_dpo_request(vacancy_url)
68
+ if not vacancy_data or not vacancy_data.get('results'):
69
+ return []
70
+ # 2. Extract car park IDs and map id -> vacancy info
71
+ id_to_vacancy = {}
72
+ for info in vacancy_data['results']:
73
+ park_id = info.get('park_Id')
74
+ if not park_id:
75
+ continue
76
+ id_to_vacancy[park_id] = {}
77
+ for vt in ['privateCar', 'LGV', 'HGV', 'coach', 'motorCycle']:
78
+ if vt in info:
79
+ vt_data = info[vt]
80
+ if isinstance(vt_data, dict):
81
+ vt_data = [vt_data]
82
+ id_to_vacancy[park_id][vt] = vt_data
83
+ park_ids = list(id_to_vacancy.keys())
84
+ if not park_ids:
85
+ return []
86
+ # 3. Second call: get details
87
+ ids_str = ','.join(park_ids)
88
+ info_url = (
89
+ f"{DPO_API_BASE}/v1/carpark-info-vacancy?data=info&lang=zh_TW"
90
+ f"&vehicleTypes={vehicle_type}"
91
+ f"&carparkIds={ids_str}"
92
+ )
93
+ info_data = await make_dpo_request(info_url)
94
+ if not info_data or not info_data.get('results'):
95
+ return []
96
+ # 4. Merge vacancy info into details
97
+ merged_results = []
98
+ for info in info_data['results']:
99
+ park_id = info.get('park_Id')
100
+ if not park_id or park_id not in id_to_vacancy:
101
+ continue
102
+ info['vacancy'] = id_to_vacancy[park_id]
103
+ merged_results.append(info)
104
+ # 5. Validate with Pydantic
105
+ try:
106
+ validated = CarParkApiResponse(results=merged_results)
107
+ carparks = validated.results
108
+ # Filter out car parks with last update > 15 mins ago for the selected vehicle type
109
+ now = datetime.datetime.now()
110
+ def is_recent(cp):
111
+ vlist = cp.vacancy.get(vehicle_type, [])
112
+ if vlist and hasattr(vlist[0], 'lastupdate') and vlist[0].lastupdate:
113
+ try:
114
+ lastupdate = datetime.datetime.strptime(vlist[0].lastupdate, "%Y-%m-%d %H:%M:%S")
115
+ return (now - lastupdate).total_seconds() <= 15 * 60
116
+ except Exception:
117
+ return False
118
+ return False
119
+ carparks = [cp for cp in carparks if is_recent(cp)]
120
+ # Filter out car parks with no vacancy for the selected vehicle type
121
+ def has_vacancy(cp):
122
+ vlist = cp.vacancy.get(vehicle_type, [])
123
+ if vlist and hasattr(vlist[0], 'vacancy') and vlist[0].vacancy is not None:
124
+ return vlist[0].vacancy > 0
125
+ return False
126
+ carparks = [cp for cp in carparks if has_vacancy(cp)]
127
+ if not carparks:
128
+ return []
129
+ # Sort by vacancy for the selected vehicle_type, descending
130
+ def get_vacancy(cp):
131
+ vlist = cp.vacancy.get(vehicle_type, [])
132
+ if vlist and hasattr(vlist[0], 'vacancy') and vlist[0].vacancy is not None:
133
+ return vlist[0].vacancy
134
+ return -1 # Treat missing/None as lowest
135
+ carparks_sorted = sorted(carparks, key=get_vacancy, reverse=True)
136
+ return carparks_sorted[:5]
137
+ except ValidationError as e:
138
+ print(f"Validation error: {e}")
139
+ return []
140
+
141
+ def format_carparks_markdown(carparks: List[CarPark]) -> str:
142
+ """
143
+ Format a list of CarPark objects into a markdown string.
144
+ Each car park will show its name, address, last update, and a table of vehicle type vacancies.
145
+ """
146
+ if not carparks:
147
+ return "目前沒有可用車位。"
148
+
149
+ lines = []
150
+ for carpark in carparks:
151
+ lines.append(f"### {carpark.name or '未知停車場'}")
152
+ lines.append(f"**地址**: {carpark.displayAddress or '無資料'}")
153
+ # Show last update for the main vehicle type (if available)
154
+ lastupdate = None
155
+ for vlist in carpark.vacancy.values():
156
+ if vlist and hasattr(vlist[0], 'lastupdate') and vlist[0].lastupdate:
157
+ lastupdate = vlist[0].lastupdate
158
+ break
159
+ if lastupdate:
160
+ lines.append(f"**最後更新**: {lastupdate}")
161
+ lines.append("")
162
+ lines.append("| 車輛類型 | 空位數量 |")
163
+ lines.append("|---|---|")
164
+ for vt, vacancies in carpark.vacancy.items():
165
+ if vacancies and isinstance(vacancies, list):
166
+ vacancy = vacancies[0].vacancy if vacancies[0].vacancy is not None else '無資料'
167
+ else:
168
+ vacancy = '無資料'
169
+ lines.append(f"| {vt} | {vacancy} |")
170
+ lines.append("\n---\n")
171
+ return "\n".join(lines)
172
 
173
+ def get_bounding_box(lat: float, lng: float) -> tuple[float, float, float, float]:
 
174
  """
175
+ Given a central point (lat, lng), return the bounding box (min_lng, min_lat, max_lng, max_lat)
176
+ that fully contains a circle of 2km radius around the point.
177
+ """
178
+ earth_radius_km = 6371.0
179
+ distance_km = 2.0
180
+
181
+ # Latitude: 1 deg ≈ 111 km
182
+ delta_lat = math.degrees(distance_km / earth_radius_km)
183
+ min_lat = lat - delta_lat
184
+ max_lat = lat + delta_lat
185
+
186
+ # Longitude delta varies with latitude
187
+ delta_lng = math.degrees(distance_km / (earth_radius_km * math.cos(math.radians(lat))))
188
+ min_lng = lng - delta_lng
189
+ max_lng = lng + delta_lng
190
+
191
+ return (min_lng, min_lat, max_lng, max_lat)
192
 
193
  demo = gr.Interface(
194
+ fn=get_carpark_vacancy,
195
+ inputs=[
196
+ gr.Textbox("22.3742", label="Latitude"),
197
+ gr.Textbox("114.185", label="Longitude"),
198
+ gr.Dropdown(
199
+ choices=[
200
+ ("私家車 (privateCar)", "privateCar"),
201
+ ("輕型貨車 (LGV)", "LGV"),
202
+ ("重型貨車 (HGV)", "HGV"),
203
+ ("旅遊巴士 (coach)", "coach"),
204
+ ("電單車 (motorCycle)", "motorCycle")
205
+ ],
206
+ value="privateCar",
207
+ label="車輛類型 (Vehicle Type)"
208
+ )
209
+ ],
210
+ outputs=[gr.Textbox()],
211
+ title="Car park vacancy in Hong Kong",
212
+ description="Enter lat, lng of the location and select vehicle type"
213
  )
214
 
215
  if __name__ == "__main__":
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio[mcp]
2
+ httpx
3
+ pydantic