Frontend UX improvements #37
							
								
								
									
										111
									
								
								backend/test.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								backend/test.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,111 @@ | |||||||
|  | import numpy as np | ||||||
|  |  | ||||||
|  | def euclidean_distance(p1, p2): | ||||||
|  |     print(p1, p2) | ||||||
|  |     return np.sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def maximize_score(places, max_distance, fixed_entry, top_k=3): | ||||||
|  |     """ | ||||||
|  |     Maximizes the total score of visited places while staying below the maximum distance. | ||||||
|  |      | ||||||
|  |     Parameters: | ||||||
|  |     places (list of tuples): Each tuple contains (score, (x, y), location). | ||||||
|  |     max_distance (float): The maximum distance that can be traveled. | ||||||
|  |     fixed_entry (tuple): The place that needs to be visited independently of its score. | ||||||
|  |     top_k (int): Number of top candidates to consider in each iteration. | ||||||
|  |      | ||||||
|  |     Returns: | ||||||
|  |     list of tuples: The visited places. | ||||||
|  |     float: The total score of the visited places. | ||||||
|  |     """ | ||||||
|  |     # Initialize total distance and score | ||||||
|  |     total_distance = 0 | ||||||
|  |     total_score = 0 | ||||||
|  |     visited_places = [] | ||||||
|  |  | ||||||
|  |     # Add the fixed entry to the visited list | ||||||
|  |     score, (x, y), _ = fixed_entry | ||||||
|  |     visited_places.append(fixed_entry) | ||||||
|  |     total_score += score | ||||||
|  |  | ||||||
|  |     # Remove the fixed entry from the list of places | ||||||
|  |     remaining_places = [place for place in places if place != fixed_entry] | ||||||
|  |  | ||||||
|  |     # Sort remaining places by score-to-distance ratio | ||||||
|  |      | ||||||
|  |     remaining_places.sort(key=lambda p: p[0] / euclidean_distance((x, y), (p[1][0], p[1][1])), reverse=True) | ||||||
|  |  | ||||||
|  |     # Add places to the visited list if they don't exceed the maximum distance | ||||||
|  |     current_location = (x, y) | ||||||
|  |     while remaining_places and total_distance < max_distance: | ||||||
|  |         # Consider top_k candidates | ||||||
|  |         candidates = remaining_places[:top_k] | ||||||
|  |         best_candidate = None | ||||||
|  |         best_score_increase = -np.inf | ||||||
|  |  | ||||||
|  |         for candidate in candidates: | ||||||
|  |             score, (cx, cy), location = candidate | ||||||
|  |             distance = euclidean_distance(current_location, (cx, cy)) | ||||||
|  |             if total_distance + distance <= max_distance: | ||||||
|  |                 score_increase = score / distance | ||||||
|  |                 if score_increase > best_score_increase: | ||||||
|  |                     best_score_increase = score_increase | ||||||
|  |                     best_candidate = candidate | ||||||
|  |  | ||||||
|  |         if best_candidate: | ||||||
|  |             visited_places.append(best_candidate) | ||||||
|  |             total_distance += euclidean_distance(current_location, best_candidate[1]) | ||||||
|  |             total_score += best_candidate[0] | ||||||
|  |             current_location = best_candidate[1] | ||||||
|  |             remaining_places.remove(best_candidate) | ||||||
|  |         else: | ||||||
|  |             break | ||||||
|  |  | ||||||
|  |     return visited_places, total_score | ||||||
|  |  | ||||||
|  | # Example usage | ||||||
|  | places = [ | ||||||
|  |     (10, (0, 0), 'A'), | ||||||
|  |     (8, (4, 2), 'B'), | ||||||
|  |     (15, (6, 4), 'C'), | ||||||
|  |     (7, (5, 6), 'D'), | ||||||
|  |     (12, (1, 8), 'E'), | ||||||
|  |     (14, (34, 10), 'F'), | ||||||
|  |     (15, (65, 12), 'G'), | ||||||
|  |     (12, (3, 14), 'H'), | ||||||
|  |     (12, (15, 1), 'I'), | ||||||
|  |     (7, (17, 4), 'J'), | ||||||
|  |     (12, (3, 3), 'K'), | ||||||
|  |     (4, (21, 22), 'L'), | ||||||
|  |     (12, (23, 24), 'M'), | ||||||
|  |     (4, (25, 26), 'N'), | ||||||
|  |     (2, (27, 28), 'O'), | ||||||
|  | ] | ||||||
|  | fixed_entry = (10, (0, 0), 'A') | ||||||
|  | max_distance = 50 | ||||||
|  |  | ||||||
|  | visited_places, total_score = maximize_score(places, max_distance, fixed_entry) | ||||||
|  | print("Visited Places:", visited_places) | ||||||
|  | print("Total Score:", total_score) | ||||||
|  |  | ||||||
|  | import matplotlib.pyplot as plt | ||||||
|  |  | ||||||
|  | # Plot the route | ||||||
|  | def plot_route(visited_places): | ||||||
|  |     x_coords = [place[1][0] for place in visited_places] | ||||||
|  |     y_coords = [place[1][1] for place in visited_places] | ||||||
|  |     labels = [place[2] for place in visited_places] | ||||||
|  |  | ||||||
|  |     plt.figure(figsize=(10, 6)) | ||||||
|  |     plt.plot(x_coords, y_coords, marker='o', linestyle='-', color='b') | ||||||
|  |     for i, label in enumerate(labels): | ||||||
|  |         plt.text(x_coords[i], y_coords[i], label, fontsize=12, ha='right') | ||||||
|  |  | ||||||
|  |     plt.title('Route of Visited Places') | ||||||
|  |     plt.xlabel('X Coordinate') | ||||||
|  |     plt.ylabel('Y Coordinate') | ||||||
|  |     plt.grid(True) | ||||||
|  |     plt.savefig('route.png') | ||||||
|  |  | ||||||
|  | plot_route(visited_places) | ||||||
| @@ -1,11 +1,20 @@ | |||||||
| import 'dart:ui'; | import 'package:anyway/constants.dart'; | ||||||
|  |  | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:auto_size_text/auto_size_text.dart'; | import 'package:auto_size_text/auto_size_text.dart'; | ||||||
|  |  | ||||||
| import 'package:anyway/structs/trip.dart'; | import 'package:anyway/structs/trip.dart'; | ||||||
| import 'package:anyway/pages/current_trip.dart'; | import 'package:anyway/pages/current_trip.dart'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | final List<String> statusTexts = [ | ||||||
|  |   'Parsing your preferences...', | ||||||
|  |   'Finding the best places...', | ||||||
|  |   'Crunching the numbers...', | ||||||
|  |   'Calculating the best route...', | ||||||
|  |   'Making sure you have a great time...', | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  |  | ||||||
| class CurrentTripLoadingIndicator extends StatefulWidget { | class CurrentTripLoadingIndicator extends StatefulWidget { | ||||||
|   final Trip trip; |   final Trip trip; | ||||||
|   const CurrentTripLoadingIndicator({ |   const CurrentTripLoadingIndicator({ | ||||||
| @@ -18,14 +27,52 @@ class CurrentTripLoadingIndicator extends StatefulWidget { | |||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| Widget bottomLoadingIndicator = Container( | class _CurrentTripLoadingIndicatorState extends State<CurrentTripLoadingIndicator> { | ||||||
|   height: 20.0, // Increase the height to take up more vertical space |   @override | ||||||
|  |   Widget build(BuildContext context) => Stack( | ||||||
|  |     fit: StackFit.expand, | ||||||
|  |     children: [ | ||||||
|  |       // In the very center of the panel, show the greeter which tells the user that the trip is being generated | ||||||
|  |       Center(child: loadingText(widget.trip)), | ||||||
|  |       // As a gimmick, and a way to show that the app is still working, show a few loading dots | ||||||
|  |       Align( | ||||||
|  |         alignment: Alignment.bottomCenter, | ||||||
|  |         child: statusText(), | ||||||
|  |       ) | ||||||
|  |     ], | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // automatically cycle through the greeter texts | ||||||
|  | class statusText extends StatefulWidget { | ||||||
|  |   const statusText({Key? key}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   _statusTextState createState() => _statusTextState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _statusTextState extends State<statusText> { | ||||||
|  |   int statusIndex = 0; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void initState() { | ||||||
|  |     super.initState(); | ||||||
|  |     Future.delayed(Duration(seconds: 5), () { | ||||||
|  |       setState(() { | ||||||
|  |         statusIndex = (statusIndex + 1) % statusTexts.length; | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return AutoSizeText( | ||||||
|  |       statusTexts[statusIndex], | ||||||
|  |       style: Theme.of(context).textTheme.labelSmall, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|   child: ImageFiltered( |  | ||||||
|     imageFilter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), // Apply blur effect |  | ||||||
|     child: Padding(padding: EdgeInsets.all(10), child: CircularProgressIndicator(),) |  | ||||||
|     ), |  | ||||||
| ); |  | ||||||
|  |  | ||||||
|  |  | ||||||
| Widget loadingText(Trip trip) => FutureBuilder( | Widget loadingText(Trip trip) => FutureBuilder( | ||||||
| @@ -34,43 +81,82 @@ Widget loadingText(Trip trip) => FutureBuilder( | |||||||
|     Widget greeter; |     Widget greeter; | ||||||
|  |  | ||||||
|     if (snapshot.hasData) { |     if (snapshot.hasData) { | ||||||
|       greeter = AutoSizeText( |       greeter = AnimatedGradientText( | ||||||
|         maxLines: 1, |         text: 'Creating your trip to ${snapshot.data}...', | ||||||
|         'Generating your trip to ${snapshot.data}...', |  | ||||||
|         style: greeterStyle, |         style: greeterStyle, | ||||||
|       ); |       ); | ||||||
|     } else if (snapshot.hasError) { |     } else if (snapshot.hasError) { | ||||||
|       // the exact error is shown in the central part of the trip overview. No need to show it here |       // the exact error is shown in the central part of the trip overview. No need to show it here | ||||||
|       greeter = AutoSizeText( |       greeter = AnimatedGradientText( | ||||||
|         maxLines: 1, |         text: 'Error while loading trip.', | ||||||
|         'Error while loading trip.', |  | ||||||
|         style: greeterStyle, |         style: greeterStyle, | ||||||
|         ); |       ); | ||||||
|     } else { |     } else { | ||||||
|       greeter = AutoSizeText( |       greeter = AnimatedGradientText( | ||||||
|         maxLines: 1, |         text: 'Creating your trip...', | ||||||
|         'Generating your trip...', |  | ||||||
|         style: greeterStyle, |         style: greeterStyle, | ||||||
|         ); |       ); | ||||||
|     } |     } | ||||||
|     return greeter; |     return greeter; | ||||||
|   } |   } | ||||||
| ); | ); | ||||||
|  |  | ||||||
|  | class AnimatedGradientText extends StatefulWidget { | ||||||
|  |   final String text; | ||||||
|  |   final TextStyle style; | ||||||
|  |  | ||||||
|  |   const AnimatedGradientText({ | ||||||
|  |     Key? key, | ||||||
|  |     required this.text, | ||||||
|  |     required this.style, | ||||||
|  |   }) : super(key: key); | ||||||
|  |  | ||||||
|  |  | ||||||
| class _CurrentTripLoadingIndicatorState extends State<CurrentTripLoadingIndicator> { |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) => Stack( |   _AnimatedGradientTextState createState() => _AnimatedGradientTextState(); | ||||||
|     fit: StackFit.expand, |  | ||||||
|     children: [ |  | ||||||
|       Center(child: loadingText(widget.trip)), |  | ||||||
|       Align( |  | ||||||
|         alignment: Alignment.bottomCenter, |  | ||||||
|         child: bottomLoadingIndicator, |  | ||||||
|       ) |  | ||||||
|     ], |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | class _AnimatedGradientTextState extends State<AnimatedGradientText> with SingleTickerProviderStateMixin { | ||||||
|  |   late AnimationController _controller; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void initState() { | ||||||
|  |     super.initState(); | ||||||
|  |     _controller = AnimationController( | ||||||
|  |       duration: const Duration(seconds: 1), | ||||||
|  |       vsync: this, | ||||||
|  |     )..repeat(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void dispose() { | ||||||
|  |     _controller.dispose(); | ||||||
|  |     super.dispose(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return AnimatedBuilder( | ||||||
|  |       animation: _controller, | ||||||
|  |       builder: (context, child) { | ||||||
|  |         return ShaderMask( | ||||||
|  |           shaderCallback: (bounds) { | ||||||
|  |             return LinearGradient( | ||||||
|  |               colors: [GRADIENT_START, GRADIENT_END, GRADIENT_START], | ||||||
|  |               stops: [ | ||||||
|  |                 _controller.value - 1.0, | ||||||
|  |                 _controller.value, | ||||||
|  |                 _controller.value + 1.0, | ||||||
|  |               ], | ||||||
|  |               tileMode: TileMode.mirror, | ||||||
|  |             ).createShader(bounds); | ||||||
|  |           }, | ||||||
|  |           child: Text( | ||||||
|  |             widget.text, | ||||||
|  |             style: widget.style, | ||||||
|  |           ), | ||||||
|  |         ); | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ final Shader textGradient = APP_GRADIENT.createShader(Rect.fromLTWH(0.0, 0.0, 20 | |||||||
| TextStyle greeterStyle = TextStyle( | TextStyle greeterStyle = TextStyle( | ||||||
|   foreground: Paint()..shader = textGradient, |   foreground: Paint()..shader = textGradient, | ||||||
|   fontWeight: FontWeight.bold, |   fontWeight: FontWeight.bold, | ||||||
|   fontSize: 26 |   fontSize: 25 | ||||||
| ); | ); | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -125,7 +125,7 @@ class LandmarkType { | |||||||
|   LandmarkType({required this.name, this.icon = const Icon(Icons.location_on)}) { |   LandmarkType({required this.name, this.icon = const Icon(Icons.location_on)}) { | ||||||
|     switch (name) { |     switch (name) { | ||||||
|       case 'sightseeing': |       case 'sightseeing': | ||||||
|         icon = const Icon(Icons.church); |         icon = const Icon(Icons.castle); | ||||||
|         break; |         break; | ||||||
|       case 'nature': |       case 'nature': | ||||||
|         icon = const Icon(Icons.eco); |         icon = const Icon(Icons.eco); | ||||||
|   | |||||||
| @@ -101,10 +101,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: collection |       name: collection | ||||||
|       sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a |       sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.18.0" |     version: "1.19.0" | ||||||
|   crypto: |   crypto: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -412,18 +412,18 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: leak_tracker |       name: leak_tracker | ||||||
|       sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" |       sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "10.0.5" |     version: "10.0.7" | ||||||
|   leak_tracker_flutter_testing: |   leak_tracker_flutter_testing: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: leak_tracker_flutter_testing |       name: leak_tracker_flutter_testing | ||||||
|       sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" |       sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.0.5" |     version: "3.0.8" | ||||||
|   leak_tracker_testing: |   leak_tracker_testing: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -708,7 +708,7 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: flutter |     description: flutter | ||||||
|     source: sdk |     source: sdk | ||||||
|     version: "0.0.99" |     version: "0.0.0" | ||||||
|   sliding_up_panel: |   sliding_up_panel: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -753,10 +753,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: stack_trace |       name: stack_trace | ||||||
|       sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" |       sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.11.1" |     version: "1.12.0" | ||||||
|   stream_channel: |   stream_channel: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -777,10 +777,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: string_scanner |       name: string_scanner | ||||||
|       sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" |       sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.2.0" |     version: "1.3.0" | ||||||
|   synchronized: |   synchronized: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -801,10 +801,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: test_api |       name: test_api | ||||||
|       sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" |       sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.7.2" |     version: "0.7.3" | ||||||
|   typed_data: |   typed_data: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -921,10 +921,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: vm_service |       name: vm_service | ||||||
|       sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" |       sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "14.2.5" |     version: "14.3.0" | ||||||
|   web: |   web: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|   | |||||||
| @@ -1,50 +0,0 @@ | |||||||
| import httpx |  | ||||||
| import json |  | ||||||
|  |  | ||||||
| base_url = "https://en.wikipedia.org/w/api.php" |  | ||||||
|  |  | ||||||
| def best_page_match(title) -> int: |  | ||||||
|     params = { |  | ||||||
|         "action": "query", |  | ||||||
|         "format": "json", |  | ||||||
|         "list": "prefixsearch", |  | ||||||
|         "pssearch": title, |  | ||||||
|     } |  | ||||||
|     response = httpx.get(base_url, params=params) |  | ||||||
|     data = response.json() |  | ||||||
|     data = data.get("query", {}).get("prefixsearch", []) |  | ||||||
|     titles_and_ids = {d["title"]: d["pageid"] for d in data} |  | ||||||
|  |  | ||||||
|     for t in titles_and_ids: |  | ||||||
|         if title.lower() == t.lower(): |  | ||||||
|             print("Matched") |  | ||||||
|             return titles_and_ids[t] |  | ||||||
|  |  | ||||||
| def get_image_url(page_id) -> str: |  | ||||||
|     # https://en.wikipedia.org/w/api.php?action=query&titles=K%C3%B6lner%20Dom&prop=imageinfo&iiprop=url&format=json |  | ||||||
|     params = { |  | ||||||
|         "action": "query", |  | ||||||
|         "format": "json", |  | ||||||
|         "prop": "pageimages", |  | ||||||
|         "pageids": page_id, |  | ||||||
|         "pithumbsize": 500, |  | ||||||
|     } |  | ||||||
|     response = httpx.get(base_url, params=params) |  | ||||||
|     data = response.json() |  | ||||||
|     data = data.get("query", {}).get("pages", {}) |  | ||||||
|     data = data.get(str(page_id), {}) |  | ||||||
|     return data.get("thumbnail", {}).get("source") |  | ||||||
|  |  | ||||||
| def get_image_url_from_title(title) -> str: |  | ||||||
|     page_id = best_page_match(title) |  | ||||||
|     if page_id is None: |  | ||||||
|         return None |  | ||||||
|     return get_image_url(page_id) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| print(get_image_url_from_title("kölner dom")) |  | ||||||
| print(get_image_url_from_title("grossmünster")) |  | ||||||
| print(get_image_url_from_title("eiffel tower")) |  | ||||||
| print(get_image_url_from_title("taj mahal")) |  | ||||||
| print(get_image_url_from_title("big ben")) |  | ||||||
|  |  | ||||||
		Reference in New Issue
	
	Block a user