Advertisement
GMan_LDN

PlacesFinder

Feb 1st, 2025
16
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 10.60 KB | None | 0 0
  1. import tkinter as tk
  2. from tkinter import ttk, messagebox
  3. import urllib.request
  4. import urllib.parse
  5. import urllib.error
  6. import json
  7. import math
  8.  
  9. # =============================================================================
  10. # Helper Function: Haversine Formula
  11. # =============================================================================
  12. def haversine(lat1, lon1, lat2, lon2):
  13. """
  14. Calculate the great-circle distance between two points on Earth (in kilometers).
  15. """
  16. R = 6371.0 # Radius of the Earth in kilometers
  17. phi1 = math.radians(lat1)
  18. phi2 = math.radians(lat2)
  19. delta_phi = math.radians(lat2 - lat1)
  20. delta_lambda = math.radians(lon2 - lon1)
  21.  
  22. a = math.sin(delta_phi/2)**2 + \
  23. math.cos(phi1) * math.cos(phi2) * math.sin(delta_lambda/2)**2
  24. c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
  25.  
  26. distance = R * c
  27. return distance
  28.  
  29. # =============================================================================
  30. # Data Source Base Class & Overpass Implementation Using urllib
  31. # =============================================================================
  32. class PlaceDataSource:
  33. """
  34. Abstract interface for a place data source.
  35. """
  36. def get_restaurants(self, lat, lon, radius_m):
  37. raise NotImplementedError
  38.  
  39. def get_hotels(self, lat, lon, radius_m):
  40. raise NotImplementedError
  41.  
  42.  
  43. class OverpassSource(PlaceDataSource):
  44. """
  45. Data source using the OpenStreetMap Overpass API.
  46. This queries for restaurants (amenity=restaurant) and hotels (tourism=hotel).
  47. """
  48. def __init__(self):
  49. self.api_url = "http://overpass-api.de/api/interpreter"
  50.  
  51. def query_overpass(self, query):
  52. """
  53. Submit a query to the Overpass API using urllib and return the JSON result.
  54. """
  55. data = urllib.parse.urlencode({'data': query}).encode('utf-8')
  56. req = urllib.request.Request(self.api_url, data=data, method="POST")
  57. try:
  58. with urllib.request.urlopen(req) as response:
  59. # Check that we received a successful status code (200)
  60. if response.getcode() == 200:
  61. response_data = response.read().decode('utf-8')
  62. return json.loads(response_data)
  63. else:
  64. raise Exception(f"Overpass API error: {response.getcode()}")
  65. except urllib.error.URLError as e:
  66. raise Exception(f"Overpass API error: {e}")
  67.  
  68. def get_restaurants(self, lat, lon, radius_m):
  69. query = f"""
  70. [out:json];
  71. (
  72. node["amenity"="restaurant"](around:{radius_m},{lat},{lon});
  73. way["amenity"="restaurant"](around:{radius_m},{lat},{lon});
  74. relation["amenity"="restaurant"](around:{radius_m},{lat},{lon});
  75. );
  76. out center;
  77. """
  78. data = self.query_overpass(query)
  79. return self._parse_elements(data)
  80.  
  81. def get_hotels(self, lat, lon, radius_m):
  82. query = f"""
  83. [out:json];
  84. (
  85. node["tourism"="hotel"](around:{radius_m},{lat},{lon});
  86. way["tourism"="hotel"](around:{radius_m},{lat},{lon});
  87. relation["tourism"="hotel"](around:{radius_m},{lat},{lon});
  88. );
  89. out center;
  90. """
  91. data = self.query_overpass(query)
  92. return self._parse_elements(data)
  93.  
  94. def _parse_elements(self, data):
  95. """
  96. Parse the returned JSON data to extract a list of places.
  97. Each place is a dict with name, coordinates, and address (if available).
  98. """
  99. places = []
  100. for element in data.get("elements", []):
  101. # For ways and relations, get center coordinates; for nodes, use lat/lon directly.
  102. if element.get("type") in ["way", "relation"]:
  103. center = element.get("center", {})
  104. lat_val = center.get("lat")
  105. lon_val = center.get("lon")
  106. else:
  107. lat_val = element.get("lat")
  108. lon_val = element.get("lon")
  109.  
  110. if lat_val is None or lon_val is None:
  111. continue
  112.  
  113. tags = element.get("tags", {})
  114. name = tags.get("name", "Unknown")
  115.  
  116. # Try to assemble an address from available tags.
  117. address_parts = []
  118. if "addr:housenumber" in tags:
  119. address_parts.append(tags["addr:housenumber"])
  120. if "addr:street" in tags:
  121. address_parts.append(tags["addr:street"])
  122. if "addr:city" in tags:
  123. address_parts.append(tags["addr:city"])
  124. address = ", ".join(address_parts)
  125.  
  126. places.append({
  127. "name": name,
  128. "lat": lat_val,
  129. "lon": lon_val,
  130. "address": address,
  131. "tags": tags
  132. })
  133. return places
  134.  
  135. # =============================================================================
  136. # GUI Functions
  137. # =============================================================================
  138. def update_table(treeview, places):
  139. """
  140. Clear the given Treeview and insert rows for each place.
  141. """
  142. for row in treeview.get_children():
  143. treeview.delete(row)
  144. for place in places:
  145. treeview.insert("", "end", values=(
  146. place['name'],
  147. f"{place['distance']:.2f}",
  148. place['address']
  149. ))
  150.  
  151. def search_places():
  152. """
  153. Called when the Search button is pressed.
  154. Reads the input, queries data sources for restaurants and hotels,
  155. processes the data, and updates the GUI tables.
  156. """
  157. status_label.config(text="Searching...")
  158. search_button.config(state="disabled")
  159. try:
  160. # Validate and parse inputs.
  161. lat = float(lat_entry.get())
  162. lon = float(lon_entry.get())
  163. distance_km = float(distance_entry.get())
  164. radius_m = int(distance_km * 1000) # Overpass API expects meters
  165.  
  166. # Initialize our data source.
  167. data_source = OverpassSource()
  168.  
  169. # Query for restaurants and hotels.
  170. restaurants = data_source.get_restaurants(lat, lon, radius_m)
  171. hotels = data_source.get_hotels(lat, lon, radius_m)
  172.  
  173. # Compute distance from the input coordinate for each result.
  174. for place in restaurants:
  175. place['distance'] = haversine(lat, lon, place['lat'], place['lon'])
  176. for place in hotels:
  177. place['distance'] = haversine(lat, lon, place['lat'], place['lon'])
  178.  
  179. # Sort by distance and select the top 20 results.
  180. restaurants = sorted(restaurants, key=lambda x: x['distance'])[:20]
  181. hotels = sorted(hotels, key=lambda x: x['distance'])[:20]
  182.  
  183. # Update the tables in the GUI.
  184. update_table(restaurant_treeview, restaurants)
  185. update_table(hotel_treeview, hotels)
  186. status_label.config(text="Search complete.")
  187. except Exception as e:
  188. messagebox.showerror("Error", str(e))
  189. status_label.config(text="Error during search.")
  190. finally:
  191. search_button.config(state="normal")
  192.  
  193. # =============================================================================
  194. # Build the GUI
  195. # =============================================================================
  196. root = tk.Tk()
  197. root.title("Nearby Places Finder")
  198.  
  199. # --- Input Frame ---
  200. input_frame = ttk.Frame(root, padding="10")
  201. input_frame.pack(pady=10, padx=10, fill="x")
  202.  
  203. ttk.Label(input_frame, text="Latitude:").grid(row=0, column=0, sticky="e", padx=5, pady=5)
  204. lat_entry = ttk.Entry(input_frame, width=20)
  205. lat_entry.grid(row=0, column=1, padx=5, pady=5)
  206.  
  207. ttk.Label(input_frame, text="Longitude:").grid(row=1, column=0, sticky="e", padx=5, pady=5)
  208. lon_entry = ttk.Entry(input_frame, width=20)
  209. lon_entry.grid(row=1, column=1, padx=5, pady=5)
  210.  
  211. ttk.Label(input_frame, text="Distance (km):").grid(row=2, column=0, sticky="e", padx=5, pady=5)
  212. distance_entry = ttk.Entry(input_frame, width=20)
  213. distance_entry.grid(row=2, column=1, padx=5, pady=5)
  214.  
  215. search_button = ttk.Button(input_frame, text="Search", command=search_places)
  216. search_button.grid(row=3, column=0, columnspan=2, pady=10)
  217.  
  218. # --- Status Label ---
  219. status_label = ttk.Label(root, text="Ready", relief="sunken", anchor="w")
  220. status_label.pack(fill="x", padx=10, pady=(0, 10))
  221.  
  222. # --- Results Frame (Two Tables) ---
  223. results_frame = ttk.Frame(root, padding="10")
  224. results_frame.pack(padx=10, pady=10, fill="both", expand=True)
  225.  
  226. # Frame for Restaurants
  227. restaurants_frame = ttk.Labelframe(results_frame, text="Restaurants", padding="5")
  228. restaurants_frame.pack(side="left", fill="both", expand=True, padx=5)
  229.  
  230. restaurant_treeview = ttk.Treeview(
  231. restaurants_frame,
  232. columns=("Name", "Distance (km)", "Address"),
  233. show="headings",
  234. selectmode="browse"
  235. )
  236. restaurant_treeview.heading("Name", text="Name")
  237. restaurant_treeview.heading("Distance (km)", text="Distance (km)")
  238. restaurant_treeview.heading("Address", text="Address")
  239. restaurant_treeview.column("Name", width=150)
  240. restaurant_treeview.column("Distance (km)", width=100, anchor="center")
  241. restaurant_treeview.column("Address", width=200)
  242. restaurant_treeview.pack(fill="both", expand=True, side="left")
  243.  
  244. # Vertical scrollbar for restaurants table.
  245. restaurant_scrollbar = ttk.Scrollbar(restaurants_frame, orient="vertical", command=restaurant_treeview.yview)
  246. restaurant_treeview.configure(yscrollcommand=restaurant_scrollbar.set)
  247. restaurant_scrollbar.pack(side="right", fill="y")
  248.  
  249. # Frame for Hotels
  250. hotels_frame = ttk.Labelframe(results_frame, text="Hotels", padding="5")
  251. hotels_frame.pack(side="right", fill="both", expand=True, padx=5)
  252.  
  253. hotel_treeview = ttk.Treeview(
  254. hotels_frame,
  255. columns=("Name", "Distance (km)", "Address"),
  256. show="headings",
  257. selectmode="browse"
  258. )
  259. hotel_treeview.heading("Name", text="Name")
  260. hotel_treeview.heading("Distance (km)", text="Distance (km)")
  261. hotel_treeview.heading("Address", text="Address")
  262. hotel_treeview.column("Name", width=150)
  263. hotel_treeview.column("Distance (km)", width=100, anchor="center")
  264. hotel_treeview.column("Address", width=200)
  265. hotel_treeview.pack(fill="both", expand=True, side="left")
  266.  
  267. # Vertical scrollbar for hotels table.
  268. hotel_scrollbar = ttk.Scrollbar(hotels_frame, orient="vertical", command=hotel_treeview.yview)
  269. hotel_treeview.configure(yscrollcommand=hotel_scrollbar.set)
  270. hotel_scrollbar.pack(side="right", fill="y")
  271.  
  272. # =============================================================================
  273. # Start the GUI event loop.
  274. # =============================================================================
  275. if __name__ == "__main__":
  276. root.mainloop()
  277.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement