Compare commits
	
		
			1 Commits
		
	
	
		
			v0.0.28
			...
			43f50ec207
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 43f50ec207 | 
| @@ -18,7 +18,7 @@ jobs: | ||||
|     name: Deploy to production | ||||
|     uses: ./.gitea/workflows/workflow_deploy-container.yaml | ||||
|     with: | ||||
|       overlay: prod | ||||
|       environment: prod | ||||
|     secrets: | ||||
|       KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }} | ||||
|       PACKAGE_REGISTRY_ACCESS: ${{ secrets.KUBE_CONFIG }} | ||||
|     needs: build-and-push | ||||
|   | ||||
| @@ -20,7 +20,7 @@ jobs: | ||||
|     name: Deploy to staging | ||||
|     uses: ./.gitea/workflows/workflow_deploy-container.yaml | ||||
|     with: | ||||
|       overlay: stg | ||||
|       environment: stg | ||||
|     secrets: | ||||
|       KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }} | ||||
|       PACKAGE_REGISTRY_ACCESS: ${{ secrets.KUBE_CONFIG }} | ||||
|     needs: build-and-push | ||||
|   | ||||
| @@ -34,5 +34,5 @@ jobs: | ||||
|       uses: docker/build-push-action@v5 | ||||
|       with: | ||||
|         context: backend | ||||
|         tags: git.kluster.moll.re/anydev/anyway-backend:${{ inputs.tag }} | ||||
|         tags: git.kluster.moll.re/anydev/anyway-backend:${{intputs.tag}} | ||||
|         push: true | ||||
|   | ||||
| @@ -32,4 +32,3 @@ jobs: | ||||
|     - name: Deploy to k8s | ||||
|       run: | | ||||
|         kubectl apply -k backend/deployment/overlays/${{ inputs.overlay }} --kubeconfig=kubeconfig | ||||
|         kubectl -n anyway-backend rollout restart deployment/anyway-backend-${{ inputs.overlay }} --kubeconfig=kubeconfig | ||||
|   | ||||
| @@ -9,9 +9,9 @@ name = "pypi" | ||||
| numpy = "*" | ||||
| fastapi = "*" | ||||
| pydantic = "*" | ||||
| geopy = "*" | ||||
| shapely = "*" | ||||
| scipy = "*" | ||||
| osmpythontools = "*" | ||||
| pywikibot = "*" | ||||
| pymemcache = "*" | ||||
| fastapi-cli = "*" | ||||
|   | ||||
							
								
								
									
										2208
									
								
								backend/Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						| @@ -9,11 +9,9 @@ This repository contains the backend code for the application. It utilizes FastA | ||||
| - Since the application is aimed to be deployed in a container, the `Dockerfile` is provided to build the image. | ||||
|  | ||||
| ### Deployment | ||||
| To deploy the backend docker container, we use kubernetes. Modifications to the backend are automatically pushed to a two-stage environment through the CI pipeline. See [deployment/README](deployment/README.md] for further information. | ||||
|  | ||||
| The deployment configuration is included as a submodule in the `deployment` directory. The standalone repository is under [https://git.kluster.moll.re/anydev/anyway-backend-deployment/](https://git.kluster.moll.re/anydev/anyway-backend-deployment/). | ||||
| To deploy the backend docker container, we use kubernetes. The deployment configuration is located under [https://git.kluster.moll.re/anydev/deployment-backend/](https://git.kluster.moll.re/anydev/deployment-backend/). | ||||
|  | ||||
|  | ||||
| ## Development | ||||
| TBD | ||||
|  | ||||
| Test for pull request | ||||
| @@ -16,7 +16,7 @@ OSM_CACHE_DIR = Path(cache_dir_string) | ||||
|  | ||||
| import logging | ||||
| # if we are in a debug session, set verbose and rich logging | ||||
| if os.getenv('DEBUG', "false") == "true": | ||||
| if os.getenv('DEBUG', False): | ||||
|     from rich.logging import RichHandler | ||||
|     logging.basicConfig( | ||||
|         level=logging.DEBUG, | ||||
|   | ||||
| @@ -63,7 +63,7 @@ def new_trip(preferences: Preferences, start: tuple[float, float], end: tuple[fl | ||||
|     refined_tour = refiner.refine_optimization(landmarks, base_tour, preferences.max_time_minute, preferences.detour_tolerance_minute) | ||||
|  | ||||
|     linked_tour = LinkedLandmarks(refined_tour) | ||||
|     # upon creation of the trip, persistence of both the trip and its landmarks is ensured | ||||
|     # upon creation of the trip, persistence of both the trip and its landmarks is ensured. Ca | ||||
|     trip = Trip.from_linked_landmarks(linked_tour, cache_client) | ||||
|     return trip | ||||
|  | ||||
| @@ -84,4 +84,4 @@ def get_landmark(landmark_uuid: str) -> Landmark: | ||||
|         landmark = cache_client.get(f"landmark_{landmark_uuid}") | ||||
|         return landmark | ||||
|     except KeyError: | ||||
|         raise HTTPException(status_code=404, detail="Landmark not found") | ||||
|         raise HTTPException(status_code=404, detail="Landmark not found") | ||||
| @@ -1,6 +1,3 @@ | ||||
| # Tags were picked mostly arbitrarily, based on the OSM wiki and the OSM tags page. | ||||
| # See https://taginfo.openstreetmap.org for more inspiration. | ||||
|  | ||||
| nature: | ||||
|   leisure: park | ||||
|   geological: '' | ||||
| @@ -14,24 +11,7 @@ nature: | ||||
|     - alpine_hut | ||||
|     - viewpoint | ||||
|     - zoo | ||||
|     - resort | ||||
|     - picnic_site | ||||
|   water: | ||||
|     - pond | ||||
|     - lake | ||||
|     - river | ||||
|     - basin | ||||
|     - stream | ||||
|     - lagoon | ||||
|     - rapids | ||||
|   waterway: | ||||
|     - waterfall | ||||
|     - river | ||||
|     - canal | ||||
|     - dam | ||||
|     - dock | ||||
|     - boatyard | ||||
|  | ||||
|   waterway: waterfall | ||||
|  | ||||
| shopping: | ||||
|   shop: | ||||
| @@ -43,48 +23,10 @@ sightseeing: | ||||
|     - museum | ||||
|     - attraction | ||||
|     - gallery | ||||
|     - artwork | ||||
|     - aquarium | ||||
|  | ||||
|   historic: '' | ||||
|   amenity: | ||||
|     - planetarium | ||||
|     - place_of_worship | ||||
|     - fountain | ||||
|     - townhall | ||||
|   water: | ||||
|     - reflecting_pool | ||||
|   bridge: | ||||
|     - aqueduct | ||||
|     - viaduct | ||||
|     - boardwalk | ||||
|     - cantilever | ||||
|     - abandoned | ||||
|   building: | ||||
|     - church | ||||
|     - chapel | ||||
|     - mosque | ||||
|     - synagogue | ||||
|     - ruins | ||||
|     - temple | ||||
|     - government | ||||
|     - cathedral | ||||
|     - castle | ||||
|     - museum | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| # to be used later on | ||||
| restauration: | ||||
|   shop: | ||||
|     - coffee | ||||
|     - bakery | ||||
|     - restaurant | ||||
|     - pastry | ||||
|   amenity: | ||||
|     - restaurant | ||||
|     - cafe | ||||
|     - ice_cream | ||||
|     - food_court | ||||
|     - biergarten | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| city_bbox_side: 7500 #m | ||||
| radius_close_to: 50 | ||||
| church_coeff: 0.5 | ||||
| church_coeff: 0.75 | ||||
| nature_coeff: 1.25 | ||||
| overall_coeff: 10 | ||||
| tag_exponent: 1.15 | ||||
|   | ||||
| @@ -2,5 +2,5 @@ detour_factor: 1.4 | ||||
| detour_corridor_width: 300 | ||||
| average_walking_speed: 4.8 | ||||
| max_landmarks: 10 | ||||
| max_landmarks_refiner: 30 | ||||
| overshoot: 1.8 | ||||
| max_landmarks_refiner: 20 | ||||
| overshoot: 1.3 | ||||
|   | ||||
| @@ -21,8 +21,8 @@ if constants.MEMCACHED_HOST_PATH is None: | ||||
| else: | ||||
|     client = Client( | ||||
|         constants.MEMCACHED_HOST_PATH, | ||||
|         timeout = 1, | ||||
|         allow_unicode_keys = True, | ||||
|         encoding = 'utf-8', | ||||
|         serde = serde.pickle_serde | ||||
|         timeout=1, | ||||
|         allow_unicode_keys=True, | ||||
|         encoding='utf-8', | ||||
|         serde=serde.pickle_serde | ||||
|     ) | ||||
|   | ||||
| @@ -5,7 +5,7 @@ from uuid import uuid4 | ||||
|  | ||||
| # Output to frontend | ||||
| class Landmark(BaseModel) : | ||||
|  | ||||
|      | ||||
|     # Properties of the landmark | ||||
|     name : str | ||||
|     type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish'] | ||||
| @@ -14,39 +14,26 @@ class Landmark(BaseModel) : | ||||
|     osm_id : int | ||||
|     attractiveness : int | ||||
|     n_tags : int | ||||
|     image_url : Optional[str] = None | ||||
|     website_url : Optional[str] = None | ||||
|     image_url : Optional[str] = None                            # TODO future | ||||
|     description : Optional[str] = None                          # TODO future | ||||
|     duration : Optional[int] = 0 | ||||
|     name_en : Optional[str] = None | ||||
|     duration : Optional[int] = 0                                # TODO future | ||||
|  | ||||
|     # Unique ID of a given landmark | ||||
|     uuid: str = Field(default_factory=uuid4) | ||||
|  | ||||
|     uuid: str = Field(default_factory=uuid4)                    # TODO implement this ASAP | ||||
|      | ||||
|     # Additional properties depending on specific tour | ||||
|     must_do : Optional[bool] = False | ||||
|     must_avoid : Optional[bool] = False | ||||
|     is_secondary : Optional[bool] = False                       # TODO future    | ||||
|  | ||||
|     time_to_reach_next : Optional[int] = 0 | ||||
|     next_uuid : Optional[str] = None | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         time_to_next_str = f", time_to_next={self.time_to_reach_next}" if self.time_to_reach_next else "" | ||||
|         is_secondary_str = f", secondary" if self.is_secondary else "" | ||||
|         type_str = '(' + self.type + ')' | ||||
|         if self.type in ["start", "finish", "nature", "shopping"] : type_str += '\t ' | ||||
|         return f'Landmark{type_str}: [{self.name} @{self.location}, score={self.attractiveness}{time_to_next_str}{is_secondary_str}]' | ||||
|  | ||||
|     def distance(self, value: 'Landmark') -> float: | ||||
|         return (self.location[0] - value.location[0])**2 + (self.location[1] - value.location[1])**2 | ||||
|      | ||||
|     time_to_reach_next : Optional[int] = 0                      # TODO fix this in existing code | ||||
|     next_uuid : Optional[str] = None                            # TODO implement this ASAP | ||||
|  | ||||
|     def __hash__(self) -> int: | ||||
|         return hash(self.name) | ||||
|  | ||||
|     def __eq__(self, value: 'Landmark') -> bool: | ||||
|         # eq and hash must be consistent | ||||
|         # in particular, if two objects are equal, their hash must be equal | ||||
|         # uuid and osm_id are just shortcuts to avoid comparing all the properties | ||||
|         # if they are equal, we know that the name is also equal and in turn the hash is equal | ||||
|         return self.uuid == value.uuid or self.osm_id == value.osm_id or (self.name == value.name and self.distance(value) < 0.001) | ||||
|         return self.uuid.int | ||||
|      | ||||
|     def __str__(self) -> str: | ||||
|         time_to_next_str = f"time_to_next={self.time_to_reach_next}" if self.time_to_reach_next else "" | ||||
|         # return f'Landmark({self.type}): [{self.name} @{self.location}, score={self.attractiveness}{time_to_next_str}]' | ||||
|         return f'({self.type[:4]}),  score={self.attractiveness}\tmain:{not self.is_secondary}\tduration={self.duration}\t{time_to_next_str}\t{self.name}' | ||||
|      | ||||
|   | ||||
| @@ -52,7 +52,7 @@ class LinkedLandmarks: | ||||
|          | ||||
|         # Update 'is_secondary' for landmarks with attractiveness below the threshold score | ||||
|         for landmark in self._landmarks: | ||||
|             if landmark.attractiveness < threshold_score and landmark.type not in ["start", "finish"]: | ||||
|             if landmark.attractiveness < threshold_score: | ||||
|                 landmark.is_secondary = True | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -22,10 +22,9 @@ class Trip(BaseModel): | ||||
|  | ||||
|         # Store the trip in the cache | ||||
|         cache_client.set(f"trip_{trip.uuid}", trip) | ||||
|         # make sure to await the result (noreply=False). Otherwise the cache might not be inplace when the trip is actually requested | ||||
|         cache_client.set_many({f"landmark_{landmark.uuid}": landmark for landmark in landmarks}, expire=3600, noreply=False) | ||||
|         cache_client.set_many({f"landmark_{landmark.uuid}": landmark for landmark in landmarks}, expire=3600) | ||||
|         # is equivalent to: | ||||
|         # for landmark in landmarks: | ||||
|         #     cache_client.set(f"landmark_{landmark.uuid}", landmark, expire=3600) | ||||
|  | ||||
|         return trip | ||||
|         return trip | ||||
| @@ -23,8 +23,9 @@ def test(start_coords: tuple[float, float], finish_coords: tuple[float, float] = | ||||
|         sightseeing=Preference(type='sightseeing', score = 5), | ||||
|         nature=Preference(type='nature', score = 5), | ||||
|         shopping=Preference(type='shopping', score = 5), | ||||
|         max_time_minute=100, | ||||
|         detour_tolerance_minute=0 | ||||
|  | ||||
|         max_time_minute=300, | ||||
|         detour_tolerance_minute=15 | ||||
|     ) | ||||
|  | ||||
|     # Create start and finish  | ||||
| @@ -63,7 +64,10 @@ def test(start_coords: tuple[float, float], finish_coords: tuple[float, float] = | ||||
|     logger.info("Optimized route : ") | ||||
|     for l in linked_tour : | ||||
|         logger.info(f"{l}") | ||||
|     logger.info(f"Estimated length of tour : {linked_tour.total_time} mintutes and visiting {len(linked_tour._landmarks)} landmarks.") | ||||
|         total_time += l.duration | ||||
|         total_time += l.time_to_reach_next | ||||
|  | ||||
|     logger.info(f"Total time: {total_time}") | ||||
|  | ||||
|     # with open('linked_tour.yaml', 'w') as f: | ||||
|     #     yaml.dump(linked_tour.asdict(), f) | ||||
| @@ -74,6 +78,6 @@ def test(start_coords: tuple[float, float], finish_coords: tuple[float, float] = | ||||
| # test(tuple((48.8344400, 2.3220540)))       # Café Chez César  | ||||
| # test(tuple((48.8375946, 2.2949904)))       # Point random | ||||
| # test(tuple((47.377859, 8.540585)))         # Zurich HB | ||||
| # test(tuple((45.758217, 4.831814)))      # Lyon Bellecour | ||||
| test(tuple((48.5848435, 7.7332974)))      # Strasbourg Gare | ||||
| test(tuple((45.7576485, 4.8330241)))      # Lyon Bellecour | ||||
| # test(tuple((48.5848435, 7.7332974)))      # Strasbourg Gare | ||||
| # test(tuple((48.2067858, 16.3692340)))      # Vienne | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import yaml | ||||
| from math import sin, cos, sqrt, atan2, radians | ||||
| from geopy.distance import geodesic | ||||
|  | ||||
| import constants | ||||
|  | ||||
| @@ -8,7 +8,6 @@ with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f: | ||||
|     DETOUR_FACTOR = parameters['detour_factor'] | ||||
|     AVERAGE_WALKING_SPEED = parameters['average_walking_speed'] | ||||
|  | ||||
| EARTH_RADIUS_KM = 6373 | ||||
|  | ||||
| def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int: | ||||
|     """ | ||||
| @@ -17,34 +16,24 @@ def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int: | ||||
|     Args: | ||||
|         p1 (Tuple[float, float]): Coordinates of the starting location. | ||||
|         p2 (Tuple[float, float]): Coordinates of the destination. | ||||
|         detour (float): Detour factor affecting the distance. | ||||
|         speed (float): Walking speed in kilometers per hour. | ||||
|  | ||||
|         Returns: | ||||
|     Returns: | ||||
|         int: Time to travel from p1 to p2 in minutes. | ||||
|     """ | ||||
|  | ||||
|  | ||||
|     if p1 == p2: | ||||
|     # Compute the straight-line distance in km | ||||
|     if p1 == p2 : | ||||
|         return 0 | ||||
|     else: | ||||
|         # Compute the distance in km along the surface of the Earth | ||||
|         # (assume spherical Earth) | ||||
|         # this is the haversine formula, stolen from stackoverflow | ||||
|         # in order to not use any external libraries | ||||
|         lat1, lon1 = radians(p1[0]), radians(p1[1]) | ||||
|         lat2, lon2 = radians(p2[0]), radians(p2[1]) | ||||
|     else:  | ||||
|         dist = geodesic(p1, p2).kilometers | ||||
|  | ||||
|         dlon = lon2 - lon1 | ||||
|         dlat = lat2 - lat1 | ||||
|  | ||||
|         a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2 | ||||
|         c = 2 * atan2(sqrt(a), sqrt(1 - a)) | ||||
|  | ||||
|         distance = EARTH_RADIUS_KM * c | ||||
|  | ||||
|     # Consider the detour factor for average an average city | ||||
|     walk_distance = distance * DETOUR_FACTOR | ||||
|     # Consider the detour factor for average cityto deterline walking distance (in km) | ||||
|     walk_dist = dist*DETOUR_FACTOR | ||||
|  | ||||
|     # Time to walk this distance (in minutes) | ||||
|     walk_time = walk_distance / AVERAGE_WALKING_SPEED * 60 | ||||
|     walk_time = walk_dist/AVERAGE_WALKING_SPEED*60 | ||||
|  | ||||
|     return round(walk_time) | ||||
|   | ||||
| @@ -1,17 +1,20 @@ | ||||
| import math | ||||
| import math as m | ||||
| import yaml | ||||
| import logging | ||||
|  | ||||
| from OSMPythonTools.overpass import Overpass, overpassQueryBuilder | ||||
| from OSMPythonTools.cachingStrategy import CachingStrategy, JSON | ||||
| from pywikibot import ItemPage, Site | ||||
| from pywikibot import config | ||||
| config.put_throttle = 0 | ||||
| config.maxlag = 0 | ||||
|  | ||||
| from structs.preferences import Preferences | ||||
| from structs.preferences import Preferences, Preference | ||||
| from structs.landmark import Landmark | ||||
| from .take_most_important import take_most_important | ||||
| import constants | ||||
|  | ||||
| # silence the overpass logger | ||||
| logging.getLogger('OSMPythonTools').setLevel(level=logging.CRITICAL) | ||||
|  | ||||
|  | ||||
|  | ||||
| class LandmarkManager: | ||||
| @@ -43,7 +46,7 @@ class LandmarkManager: | ||||
|             self.viewpoint_bonus = parameters['viewpoint_bonus'] | ||||
|             self.pay_bonus = parameters['pay_bonus'] | ||||
|             self.N_important = parameters['N_important'] | ||||
|  | ||||
|              | ||||
|         with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f: | ||||
|             parameters = yaml.safe_load(f) | ||||
|             self.walking_speed = parameters['average_walking_speed'] | ||||
| @@ -66,42 +69,87 @@ class LandmarkManager: | ||||
|         preferences (Preferences): The user's preference settings that influence the landmark selection. | ||||
|  | ||||
|         Returns: | ||||
|         tuple[list[Landmark], list[Landmark]]: | ||||
|         - A list of all existing landmarks. | ||||
|         - A list of the most important landmarks based on the user's preferences. | ||||
|             tuple[list[Landmark], list[Landmark]]: | ||||
|                 - A list of all existing landmarks. | ||||
|                 - A list of the most important landmarks based on the user's preferences. | ||||
|         """ | ||||
|  | ||||
|         max_walk_dist = (preferences.max_time_minute/2)/60*self.walking_speed*1000/self.detour_factor | ||||
|         reachable_bbox_side = min(max_walk_dist, self.max_bbox_side) | ||||
|  | ||||
|         # use set to avoid duplicates, this requires some __methods__ to be set in Landmark | ||||
|         all_landmarks = set() | ||||
|  | ||||
|         L = [] | ||||
|         bbox = self.create_bbox(center_coordinates, reachable_bbox_side) | ||||
|         # list for sightseeing | ||||
|         if preferences.sightseeing.score != 0: | ||||
|             score_function = lambda score: score * 10 * preferences.sightseeing.score / 5 | ||||
|             current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], preferences.sightseeing.type, score_function) | ||||
|             all_landmarks.update(current_landmarks) | ||||
|             score_function = lambda score: int(score*10)*preferences.sightseeing.score/5   # self.count_elements_close_to(loc) + | ||||
|             L1 = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], preferences.sightseeing.type, score_function) | ||||
|             L += L1 | ||||
|  | ||||
|         # list for nature | ||||
|         if preferences.nature.score != 0: | ||||
|             score_function = lambda score: score * 10 * self.nature_coeff * preferences.nature.score / 5 | ||||
|             current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], preferences.nature.type, score_function) | ||||
|             all_landmarks.update(current_landmarks) | ||||
|             score_function = lambda score: int(score*10*self.nature_coeff)*preferences.nature.score/5   # self.count_elements_close_to(loc) + | ||||
|             L2 = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], preferences.nature.type, score_function) | ||||
|             L += L2 | ||||
|  | ||||
|         # list for shopping | ||||
|         if preferences.shopping.score != 0: | ||||
|             score_function = lambda score: score * 10 * preferences.shopping.score / 5 | ||||
|             current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function) | ||||
|             all_landmarks.update(current_landmarks) | ||||
|             score_function = lambda score: int(score*10)*preferences.shopping.score/5 # self.count_elements_close_to(loc) + | ||||
|             L3 = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function) | ||||
|             L += L3 | ||||
|  | ||||
|  | ||||
|         landmarks_constrained = take_most_important(all_landmarks, self.N_important) | ||||
|         self.logger.info(f'Generated {len(all_landmarks)} landmarks around {center_coordinates}, and constrained to {len(landmarks_constrained)} most important ones.') | ||||
|         L = self.remove_duplicates(L) | ||||
|         # self.correct_score(L, preferences) | ||||
|  | ||||
|         return all_landmarks, landmarks_constrained | ||||
|         L_constrained = take_most_important(L, self.N_important) | ||||
|         self.logger.info(f'Generated {len(L)} landmarks around {center_coordinates}, and constrained to {len(L_constrained)} most important ones.') | ||||
|  | ||||
|         return L, L_constrained | ||||
|  | ||||
|  | ||||
|     def remove_duplicates(self, landmarks: list[Landmark]) -> list[Landmark]: | ||||
|         """ | ||||
|         Removes duplicate landmarks based on their names from the given list. Only retains the landmark with highest score | ||||
|  | ||||
|         Parameters: | ||||
|         landmarks (list[Landmark]): A list of Landmark objects. | ||||
|  | ||||
|         Returns: | ||||
|         list[Landmark]: A list of unique Landmark objects based on their names. | ||||
|         """ | ||||
|  | ||||
|         L_clean = [] | ||||
|         names = [] | ||||
|  | ||||
|         for landmark in landmarks: | ||||
|             if landmark.name in names:  | ||||
|                 continue   | ||||
|             else: | ||||
|                 names.append(landmark.name) | ||||
|                 L_clean.append(landmark) | ||||
|          | ||||
|         return L_clean | ||||
|          | ||||
|  | ||||
|     def correct_score(self, landmarks: list[Landmark], preferences: Preferences) -> None: | ||||
|         """ | ||||
|         Adjust the attractiveness score of each landmark in the list based on user preferences. | ||||
|  | ||||
|         This method updates the attractiveness of each landmark by scaling it according to the user's preference score. | ||||
|         The score adjustment is computed using a simple linear transformation based on the preference score. | ||||
|  | ||||
|         Args: | ||||
|             landmarks (list[Landmark]): A list of landmarks whose scores need to be corrected. | ||||
|             preferences (Preferences): The user's preference settings that influence the attractiveness score adjustment. | ||||
|         """ | ||||
|  | ||||
|         score_dict = { | ||||
|             preferences.sightseeing.type: preferences.sightseeing.score, | ||||
|             preferences.nature.type: preferences.nature.score, | ||||
|             preferences.shopping.type: preferences.shopping.score | ||||
|         } | ||||
|         for landmark in landmarks: | ||||
|             landmark.attractiveness = int(landmark.attractiveness * score_dict[landmark.type] / 5)         | ||||
|  | ||||
|  | ||||
|     def count_elements_close_to(self, coordinates: tuple[float, float]) -> int: | ||||
| @@ -124,7 +172,7 @@ class LandmarkManager: | ||||
|  | ||||
|         radius = self.radius_close_to | ||||
|  | ||||
|         alpha = (180 * radius) / (6371000 * math.pi) | ||||
|         alpha = (180*radius) / (6371000*m.pi) | ||||
|         bbox = {'latLower':lat-alpha,'lonLower':lon-alpha,'latHigher':lat+alpha,'lonHigher': lon+alpha} | ||||
|  | ||||
|         # Build the query to find elements within the radius | ||||
| @@ -168,7 +216,7 @@ class LandmarkManager: | ||||
|  | ||||
|         # Convert distance to degrees | ||||
|         lat_diff = half_side_length_km / 111  # 1 degree latitude is approximately 111 km | ||||
|         lon_diff = half_side_length_km / (111 * math.cos(math.radians(lat)))  # Adjust for longitude based on latitude | ||||
|         lon_diff = half_side_length_km / (111 * m.cos(m.radians(lat)))  # Adjust for longitude based on latitude | ||||
|  | ||||
|         # Calculate bbox | ||||
|         min_lat = lat - lat_diff | ||||
| @@ -207,32 +255,32 @@ class LandmarkManager: | ||||
|             query = overpassQueryBuilder( | ||||
|                 bbox = bbox, | ||||
|                 elementType = ['way', 'relation'], | ||||
|                 # selector can in principle be a list already, | ||||
|                 # but it generates the intersection of the queries | ||||
|                 # we want the union | ||||
|                 selector = sel, | ||||
|                 conditions = ['count_tags()>5'], | ||||
|                 # conditions = [], | ||||
|                 includeCenter = True, | ||||
|                 out = 'body' | ||||
|                 ) | ||||
|             self.logger.debug(f"Query: {query}") | ||||
|  | ||||
|             try: | ||||
|                 result = self.overpass.query(query) | ||||
|             except Exception as e: | ||||
|                 self.logger.error(f"Error fetching landmarks: {e}") | ||||
|                 continue | ||||
|  | ||||
|                 return | ||||
|              | ||||
|             for elem in result.elements(): | ||||
|  | ||||
|                 name = elem.tag('name') | ||||
|                 location = (elem.centerLat(), elem.centerLon()) | ||||
|                 name = elem.tag('name')                             # Add name | ||||
|                 location = (elem.centerLat(), elem.centerLon())     # Add coordinates (lat, lon) | ||||
|  | ||||
|                 # TODO: exclude these from the get go | ||||
|                 # skip if unprecise location | ||||
|                 if name is None or location[0] is None: | ||||
|                     continue | ||||
|  | ||||
|                 # skip if unused | ||||
|                 # if 'disused:leisure' in elem.tags().keys(): | ||||
|                 #     continue | ||||
|                  | ||||
|                 # skip if part of another building | ||||
|                 if 'building:part' in elem.tags().keys() and elem.tag('building:part') == 'yes': | ||||
|                     continue | ||||
| @@ -242,29 +290,31 @@ class LandmarkManager: | ||||
|                 elem_type = landmarktype            # Add the landmark type as 'sightseeing,  | ||||
|                 n_tags = len(elem.tags().keys())    # Add number of tags | ||||
|                 score = n_tags**self.tag_exponent   # Add score | ||||
|                 website_url = None | ||||
|                 image_url = None | ||||
|                 name_en = None | ||||
|  | ||||
|  | ||||
|                 # remove specific tags | ||||
|                 skip = False | ||||
|                 for tag in elem.tags().keys(): | ||||
|                     if "pay" in tag: | ||||
|                         # payment options are a good sign | ||||
|                         score += self.pay_bonus | ||||
|                         score += self.pay_bonus             # discard payment options for tags | ||||
|  | ||||
|                     if "disused" in tag: | ||||
|                         # skip disused amenities | ||||
|                         skip = True | ||||
|                         skip = True             # skip disused amenities | ||||
|                         break | ||||
|  | ||||
|                     if "wiki" in tag: | ||||
|                         # wikipedia entries count more | ||||
|                         score += self.wikipedia_bonus | ||||
|                         score += self.wikipedia_bonus             # wikipedia entries count more | ||||
|  | ||||
|                     # if tag == "wikidata": | ||||
|                     #     Q = elem.tag('wikidata') | ||||
|                     #     site = Site("wikidata", "wikidata") | ||||
|                     #     item = ItemPage(site, Q) | ||||
|                     #     item.get() | ||||
|                     #     n_languages = len(item.labels) | ||||
|                     #     n_tags += n_languages/10 | ||||
|  | ||||
|                     if "viewpoint" in tag: | ||||
|                         score += self.viewpoint_bonus | ||||
|                         duration = 10 | ||||
|  | ||||
|                     if "image" in tag: | ||||
|                         score += self.image_bonus | ||||
| @@ -281,43 +331,38 @@ class LandmarkManager: | ||||
|                         if tag == "building" and elem.tag('building') in ['retail', 'supermarket', 'parking']: | ||||
|                             skip = True | ||||
|                             break | ||||
|  | ||||
|                     if tag in ['website', 'contact:website']: | ||||
|                         website_url = elem.tag(tag) | ||||
|                     if tag == 'image': | ||||
|                         image_url = elem.tag('image') | ||||
|                     if tag =='name:en': | ||||
|                         name_en = elem.tag('name:en') | ||||
|  | ||||
|                 if skip: | ||||
|                     continue | ||||
|  | ||||
|                 score = score_function(score) | ||||
|                 if "place_of_worship" in elem.tags().values(): | ||||
|                     score = score * self.church_coeff | ||||
|                     duration = 15 | ||||
|                 if "place_of_worship" in elem.tags().values() : | ||||
|                     score = int(score*self.church_coeff) | ||||
|                     duration = 20 | ||||
|                  | ||||
|                 elif "museum" in elem.tags().values(): | ||||
|                     score = score * self.church_coeff | ||||
|                 elif "museum" in elem.tags().values() : | ||||
|                     score = int(score*self.church_coeff) | ||||
|                     duration = 60 | ||||
|                  | ||||
|                 else: | ||||
|                 elif "fountain" in elem.tags().values() : | ||||
|                     duration = 5 | ||||
|                  | ||||
|                 elif "park" in elem.tags().values() : | ||||
|                     duration = 30 | ||||
|                  | ||||
|                 else :  | ||||
|                     duration = 15 | ||||
|  | ||||
|                 # finally create our own landmark object | ||||
|                 # Generate the landmark and append it to the list | ||||
|                 landmark = Landmark( | ||||
|                     name = name, | ||||
|                     type = elem_type, | ||||
|                     location = location, | ||||
|                     osm_type = osm_type, | ||||
|                     osm_id = osm_id, | ||||
|                     attractiveness = int(score), | ||||
|                     must_do = False, | ||||
|                     n_tags = int(n_tags), | ||||
|                     duration = int(duration), | ||||
|                     name_en = name_en, | ||||
|                     image_url = image_url, | ||||
|                     website_url = website_url | ||||
|                     name=name, | ||||
|                     type=elem_type, | ||||
|                     location=location, | ||||
|                     osm_type=osm_type, | ||||
|                     osm_id=osm_id, | ||||
|                     attractiveness=score, | ||||
|                     must_do=False, | ||||
|                     n_tags=int(n_tags), | ||||
|                     duration = duration | ||||
|                 ) | ||||
|                 return_list.append(landmark) | ||||
|          | ||||
| @@ -341,7 +386,7 @@ def dict_to_selector_list(d: dict) -> list: | ||||
|     for key, value in d.items(): | ||||
|         if type(value) == list: | ||||
|             val = '|'.join(value) | ||||
|             return_list.append(f'{key}~"^({val})$"') | ||||
|             return_list.append(f'{key}~"{val}"') | ||||
|         elif type(value) == str and len(value) == 0: | ||||
|             return_list.append(f'{key}') | ||||
|         else: | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import numpy as np | ||||
|  | ||||
| from scipy.optimize import linprog | ||||
| from collections import defaultdict, deque | ||||
| from geopy.distance import geodesic | ||||
|  | ||||
| from structs.landmark import Landmark | ||||
| from .get_time_separation import get_time | ||||
| @@ -20,7 +21,7 @@ class Optimizer: | ||||
|     detour_factor: float            # detour factor of straight line vs real distance in cities | ||||
|     average_walking_speed: float    # average walking speed of adult | ||||
|     max_landmarks: int              # max number of landmarks to visit | ||||
|     overshoot: float                # overshoot to allow maxtime to overflow. Optimizer is a bit restrictive | ||||
|     overshoot: float                # experimentally determined overshoot possibility to return long enough tours | ||||
|  | ||||
|  | ||||
|     def __init__(self) : | ||||
| @@ -177,7 +178,7 @@ class Optimizer: | ||||
|  | ||||
|         Args: | ||||
|             landmarks (list[Landmark]): List of landmarks. | ||||
|             max_time (int): Maximum time of visit allowed. | ||||
|             max_time (int): Maximum time allowed for tour. | ||||
|  | ||||
|         Returns: | ||||
|             Tuple[list[float], list[float], list[int]]: Objective function coefficients, inequality constraint coefficients, and the right-hand side of the inequality constraint. | ||||
| @@ -194,7 +195,7 @@ class Optimizer: | ||||
|             for j, spot2 in enumerate(landmarks) : | ||||
|                 t = get_time(spot1.location, spot2.location) + spot1.duration | ||||
|                 dist_table[j] = t | ||||
|             closest = sorted(dist_table)[:25] | ||||
|             closest = sorted(dist_table)[:22] | ||||
|             for i, dist in enumerate(dist_table) : | ||||
|                 if dist not in closest : | ||||
|                     dist_table[i] = 32700 | ||||
| @@ -475,7 +476,7 @@ class Optimizer: | ||||
|         A, b = self.respect_start_finish(L)                  # Force start and finish positions | ||||
|         A_eq = np.vstack((A_eq, A), dtype=np.int8) | ||||
|         b_eq += b | ||||
|         A, b = self.respect_order(L)                         # Respect order of visit (only works when max_time is limiting factor) | ||||
|         A, b = self.respect_order(L)                         # Respect order of visit (only works when max_steps is limiting factor) | ||||
|         A_eq = np.vstack((A_eq, A), dtype=np.int8) | ||||
|         b_eq += b | ||||
|          | ||||
|   | ||||
| @@ -214,7 +214,7 @@ class Refiner : | ||||
|             if self.is_in_area(area, landmark.location) and landmark.name not in visited_names: | ||||
|                 second_order_landmarks.append(landmark) | ||||
|  | ||||
|         return take_most_important.take_most_important(second_order_landmarks, int(self.max_landmarks_refiner*0.75)) | ||||
|         return take_most_important.take_most_important(second_order_landmarks, len(visited_landmarks)) | ||||
|  | ||||
|  | ||||
|     # Try fix the shortest path using shapely | ||||
|   | ||||
| @@ -1,16 +1,38 @@ | ||||
| from structs.landmark import Landmark | ||||
|  | ||||
| def take_most_important(landmarks: list[Landmark], n_important) -> list[Landmark]: | ||||
|     """ | ||||
|     Given a list of landmarks, return the n_important most important landmarks | ||||
|     Parameters: | ||||
|     landmarks: list[Landmark] - list of landmarks | ||||
|     n_important: int - number of most important landmarks to return | ||||
|     Returns: | ||||
|     list[Landmark] - list of the n_important most important landmarks | ||||
|     """ | ||||
| def take_most_important(landmarks: list[Landmark], N_important) -> list[Landmark] : | ||||
|     L = len(landmarks) | ||||
|     L_copy = [] | ||||
|     L_clean = [] | ||||
|     scores = [0]*len(landmarks) | ||||
|     names = [] | ||||
|     name_id = {} | ||||
|  | ||||
|     # Sort landmarks by attractiveness (descending) | ||||
|     sorted_landmarks = sorted(landmarks, key=lambda x: x.attractiveness, reverse=True) | ||||
|     for i, elem in enumerate(landmarks) : | ||||
|         if elem.name not in names : | ||||
|             names.append(elem.name) | ||||
|             name_id[elem.name] = [i] | ||||
|             L_copy.append(elem) | ||||
|         else : | ||||
|             name_id[elem.name] += [i] | ||||
|             scores = [] | ||||
|             for j in name_id[elem.name] : | ||||
|                 scores.append(L[j].attractiveness) | ||||
|             best_id = max(range(len(scores)), key=scores.__getitem__) | ||||
|             t = name_id[elem.name][best_id] | ||||
|             if t == i : | ||||
|                 for old in L_copy : | ||||
|                     if old.name == elem.name : | ||||
|                         old.attractiveness = L[t].attractiveness | ||||
|      | ||||
|     scores = [0]*len(L_copy) | ||||
|     for i, elem in enumerate(L_copy) : | ||||
|         scores[i] = elem.attractiveness | ||||
|  | ||||
|     return sorted_landmarks[:n_important] | ||||
|     res = sorted(range(len(scores)), key = lambda sub: scores[sub])[-(N_important-L):] | ||||
|  | ||||
|     for i, elem in enumerate(L_copy) : | ||||
|         if i in res : | ||||
|             L_clean.append(elem) | ||||
|  | ||||
|     return L_clean | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| on: | ||||
|   push: | ||||
|     tags: | ||||
|       - 'v*' | ||||
|     branches: | ||||
|       - main | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
| @@ -23,13 +23,6 @@ jobs: | ||||
|  | ||||
|       - name: Setup android SDK | ||||
|         uses: android-actions/setup-android@v3 | ||||
|        | ||||
|       - name: Install Flutter | ||||
|         uses: subosito/flutter-action@v2 | ||||
|         with: | ||||
|           channel: stable | ||||
|           flutter-version: 3.22.0 | ||||
|           cache: true | ||||
|  | ||||
|       - name: Infer version number from git tag | ||||
|         id: version | ||||
| @@ -37,13 +30,13 @@ jobs: | ||||
|           REF_NAME: ${{ github.ref_name }} | ||||
|         run: | ||||
|           # remove the 'v' prefix from the tag name | ||||
|           echo "VERSION_NAME=${REF_NAME//v}" >> $GITHUB_ENV | ||||
|           echo "VERSION=${REF_NAME//v}" >> $GITHUB_OUTPUT | ||||
|  | ||||
|       - name: Load secrets from github | ||||
|         run: | | ||||
|           echo "${{ secrets.ANDROID_SECRET_PROPERTIES_BASE64 }}" | base64 -d > secrets.properties | ||||
|           echo "${{ secrets.ANDROID_GOOGLE_PLAY_JSON_BASE64 }}" | base64 -d > google-key.json | ||||
|           echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > release.keystore | ||||
|           echo "${{ secrets.ANDROID_SECRET_PROPERTIES }}" > secrets.properties | ||||
|           echo "${{ secrets.ANDROID_KEYSTORE }}" > release.keystore | ||||
|           echo "${{ secrets.ANDROID_GOOGLE_PLAY_JSON }}" > google-key.json | ||||
|         working-directory: android | ||||
|  | ||||
|       - name: Install fastlane | ||||
| @@ -51,6 +44,7 @@ jobs: | ||||
|         working-directory: android | ||||
|  | ||||
|       - name: Run fastlane lane | ||||
|         run: bundle exec fastlane deploy_testing | ||||
|         run: bundle exec fastlane deploy_release | ||||
|         working-directory: android | ||||
|         # the environment variable VERSION_NAME is implicitly available | ||||
|         env: | ||||
|           VERSION_NAME: ${{ steps.version.VERSION }} | ||||
|   | ||||
							
								
								
									
										4
									
								
								frontend/android/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,6 +1,4 @@ | ||||
| gradlew | ||||
| gradlew.bat | ||||
| gradle/ | ||||
| gradle-wrapper.jar | ||||
| /.gradle | ||||
| /captures/ | ||||
| /local.properties | ||||
|   | ||||
| @@ -1,9 +1,6 @@ | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <!-- Required to fetch data from the internet. --> | ||||
|     <uses-permission android:name="android.permission.INTERNET"/> | ||||
|     <!-- Required to show user location --> | ||||
|     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> | ||||
|     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> | ||||
|  | ||||
|     <application | ||||
|         android:label="anyway" | ||||
|   | ||||
| @@ -4,19 +4,18 @@ | ||||
| default_platform(:android) | ||||
|  | ||||
| platform :android do | ||||
|   # desc "Runs all the tests" | ||||
|   # lane :test do | ||||
|   #   gradle(task: "test") | ||||
|   # end | ||||
|  | ||||
|   desc "Deploy a new version as a preview version" | ||||
|   lane :deploy_testing do | ||||
|     version_name = ENV["VERSION_NAME"] | ||||
|  | ||||
|     sh( | ||||
|       "flutter", | ||||
|       "build", | ||||
|       "appbundle", | ||||
|       "--release", | ||||
|       "--build-name=#{version_name}", | ||||
|     gradle( | ||||
|       task: "bundle", | ||||
|       # flavor: "staging", | ||||
|       ) | ||||
|      | ||||
|  | ||||
|     upload_to_play_store( | ||||
|       track: 'alpha', | ||||
|       skip_upload_apk: true, | ||||
|   | ||||
| After Width: | Height: | Size: 638 KiB | 
| After Width: | Height: | Size: 893 KiB | 
| After Width: | Height: | Size: 876 KiB | 
| After Width: | Height: | Size: 123 KiB | 
| After Width: | Height: | Size: 96 KiB | 
| After Width: | Height: | Size: 209 KiB | 
| Before Width: | Height: | Size: 106 KiB | 
| Before Width: | Height: | Size: 1.3 MiB | 
| Before Width: | Height: | Size: 637 KiB | 
| Before Width: | Height: | Size: 573 KiB | 
| Before Width: | Height: | Size: 175 KiB | 
| Before Width: | Height: | Size: 360 KiB | 
| After Width: | Height: | Size: 638 KiB | 
| After Width: | Height: | Size: 893 KiB | 
| After Width: | Height: | Size: 876 KiB | 
| After Width: | Height: | Size: 123 KiB | 
| After Width: | Height: | Size: 96 KiB | 
| After Width: | Height: | Size: 209 KiB | 
| After Width: | Height: | Size: 638 KiB | 
| After Width: | Height: | Size: 893 KiB | 
| After Width: | Height: | Size: 876 KiB | 
| After Width: | Height: | Size: 123 KiB | 
| After Width: | Height: | Size: 96 KiB | 
| After Width: | Height: | Size: 209 KiB | 
							
								
								
									
										5
									
								
								frontend/android/gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | ||||
| distributionBase=GRADLE_USER_HOME | ||||
| distributionPath=wrapper/dists | ||||
| zipStoreBase=GRADLE_USER_HOME | ||||
| zipStorePath=wrapper/dists | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip | ||||
							
								
								
									
										160
									
								
								frontend/android/gradlew
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						| @@ -0,0 +1,160 @@ | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| ############################################################################## | ||||
| ## | ||||
| ##  Gradle start up script for UN*X | ||||
| ## | ||||
| ############################################################################## | ||||
|  | ||||
| # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. | ||||
| DEFAULT_JVM_OPTS="" | ||||
|  | ||||
| APP_NAME="Gradle" | ||||
| APP_BASE_NAME=`basename "$0"` | ||||
|  | ||||
| # Use the maximum available, or set MAX_FD != -1 to use that value. | ||||
| MAX_FD="maximum" | ||||
|  | ||||
| warn ( ) { | ||||
|     echo "$*" | ||||
| } | ||||
|  | ||||
| die ( ) { | ||||
|     echo | ||||
|     echo "$*" | ||||
|     echo | ||||
|     exit 1 | ||||
| } | ||||
|  | ||||
| # OS specific support (must be 'true' or 'false'). | ||||
| cygwin=false | ||||
| msys=false | ||||
| darwin=false | ||||
| case "`uname`" in | ||||
|   CYGWIN* ) | ||||
|     cygwin=true | ||||
|     ;; | ||||
|   Darwin* ) | ||||
|     darwin=true | ||||
|     ;; | ||||
|   MINGW* ) | ||||
|     msys=true | ||||
|     ;; | ||||
| esac | ||||
|  | ||||
| # Attempt to set APP_HOME | ||||
| # Resolve links: $0 may be a link | ||||
| PRG="$0" | ||||
| # Need this for relative symlinks. | ||||
| while [ -h "$PRG" ] ; do | ||||
|     ls=`ls -ld "$PRG"` | ||||
|     link=`expr "$ls" : '.*-> \(.*\)$'` | ||||
|     if expr "$link" : '/.*' > /dev/null; then | ||||
|         PRG="$link" | ||||
|     else | ||||
|         PRG=`dirname "$PRG"`"/$link" | ||||
|     fi | ||||
| done | ||||
| SAVED="`pwd`" | ||||
| cd "`dirname \"$PRG\"`/" >/dev/null | ||||
| APP_HOME="`pwd -P`" | ||||
| cd "$SAVED" >/dev/null | ||||
|  | ||||
| CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar | ||||
|  | ||||
| # Determine the Java command to use to start the JVM. | ||||
| if [ -n "$JAVA_HOME" ] ; then | ||||
|     if [ -x "$JAVA_HOME/jre/sh/java" ] ; then | ||||
|         # IBM's JDK on AIX uses strange locations for the executables | ||||
|         JAVACMD="$JAVA_HOME/jre/sh/java" | ||||
|     else | ||||
|         JAVACMD="$JAVA_HOME/bin/java" | ||||
|     fi | ||||
|     if [ ! -x "$JAVACMD" ] ; then | ||||
|         die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME | ||||
|  | ||||
| Please set the JAVA_HOME variable in your environment to match the | ||||
| location of your Java installation." | ||||
|     fi | ||||
| else | ||||
|     JAVACMD="java" | ||||
|     which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. | ||||
|  | ||||
| Please set the JAVA_HOME variable in your environment to match the | ||||
| location of your Java installation." | ||||
| fi | ||||
|  | ||||
| # Increase the maximum file descriptors if we can. | ||||
| if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then | ||||
|     MAX_FD_LIMIT=`ulimit -H -n` | ||||
|     if [ $? -eq 0 ] ; then | ||||
|         if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then | ||||
|             MAX_FD="$MAX_FD_LIMIT" | ||||
|         fi | ||||
|         ulimit -n $MAX_FD | ||||
|         if [ $? -ne 0 ] ; then | ||||
|             warn "Could not set maximum file descriptor limit: $MAX_FD" | ||||
|         fi | ||||
|     else | ||||
|         warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" | ||||
|     fi | ||||
| fi | ||||
|  | ||||
| # For Darwin, add options to specify how the application appears in the dock | ||||
| if $darwin; then | ||||
|     GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" | ||||
| fi | ||||
|  | ||||
| # For Cygwin, switch paths to Windows format before running java | ||||
| if $cygwin ; then | ||||
|     APP_HOME=`cygpath --path --mixed "$APP_HOME"` | ||||
|     CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` | ||||
|     JAVACMD=`cygpath --unix "$JAVACMD"` | ||||
|  | ||||
|     # We build the pattern for arguments to be converted via cygpath | ||||
|     ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` | ||||
|     SEP="" | ||||
|     for dir in $ROOTDIRSRAW ; do | ||||
|         ROOTDIRS="$ROOTDIRS$SEP$dir" | ||||
|         SEP="|" | ||||
|     done | ||||
|     OURCYGPATTERN="(^($ROOTDIRS))" | ||||
|     # Add a user-defined pattern to the cygpath arguments | ||||
|     if [ "$GRADLE_CYGPATTERN" != "" ] ; then | ||||
|         OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" | ||||
|     fi | ||||
|     # Now convert the arguments - kludge to limit ourselves to /bin/sh | ||||
|     i=0 | ||||
|     for arg in "$@" ; do | ||||
|         CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` | ||||
|         CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option | ||||
|  | ||||
|         if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition | ||||
|             eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` | ||||
|         else | ||||
|             eval `echo args$i`="\"$arg\"" | ||||
|         fi | ||||
|         i=$((i+1)) | ||||
|     done | ||||
|     case $i in | ||||
|         (0) set -- ;; | ||||
|         (1) set -- "$args0" ;; | ||||
|         (2) set -- "$args0" "$args1" ;; | ||||
|         (3) set -- "$args0" "$args1" "$args2" ;; | ||||
|         (4) set -- "$args0" "$args1" "$args2" "$args3" ;; | ||||
|         (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; | ||||
|         (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; | ||||
|         (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; | ||||
|         (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; | ||||
|         (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; | ||||
|     esac | ||||
| fi | ||||
|  | ||||
| # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules | ||||
| function splitJvmOpts() { | ||||
|     JVM_OPTS=("$@") | ||||
| } | ||||
| eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS | ||||
| JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" | ||||
|  | ||||
| exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" | ||||
| @@ -20,7 +20,7 @@ pluginManagement { | ||||
| plugins { | ||||
|     id "dev.flutter.flutter-plugin-loader" version "1.0.0" | ||||
|     id "com.android.application" version "7.3.0" apply false | ||||
|     id "org.jetbrains.kotlin.android" version "2.0.20" apply false | ||||
|     id "org.jetbrains.kotlin.android" version "1.7.10" apply false | ||||
| } | ||||
|  | ||||
| include ":app" | ||||
|   | ||||
| @@ -1,3 +0,0 @@ | ||||
| description: This file stores settings for Dart & Flutter DevTools. | ||||
| documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states | ||||
| extensions: | ||||
| @@ -1,86 +1,6 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
| const String APP_NAME = 'AnyWay'; | ||||
|  | ||||
| String API_URL_BASE = 'https://anyway.anydev.info'; | ||||
| String API_URL_DEBUG = 'https://anyway.anydev.info'; | ||||
| String PRIVACY_URL = 'https://anydev.info/privacy'; | ||||
|  | ||||
| const String MAP_ID = '41c21ac9b81dbfd8'; | ||||
|  | ||||
|  | ||||
| const Color GRADIENT_START = Color(0xFFF9B208); | ||||
| const Color GRADIENT_END = Color(0xFFE72E77); | ||||
|  | ||||
| const Color PRIMARY_COLOR = Color(0xFFF38F1A); | ||||
|  | ||||
|  | ||||
|  | ||||
| const double TRIP_PANEL_MAX_HEIGHT = 0.8; | ||||
| const double TRIP_PANEL_MIN_HEIGHT = 0.12; | ||||
|  | ||||
| ThemeData APP_THEME = ThemeData( | ||||
|   primaryColor: PRIMARY_COLOR, | ||||
|  | ||||
|   scaffoldBackgroundColor: Colors.white, | ||||
|   cardColor: Colors.white, | ||||
|   useMaterial3: true, | ||||
|  | ||||
|   colorScheme: ColorScheme.light( | ||||
|     primary: PRIMARY_COLOR, | ||||
|     secondary: GRADIENT_END, | ||||
|     surface: Colors.white, | ||||
|     error: Colors.red, | ||||
|     onPrimary: Colors.white, | ||||
|     onSecondary: const Color.fromARGB(255, 30, 22, 22), | ||||
|     onSurface: Colors.black, | ||||
|     onError: Colors.white, | ||||
|     brightness: Brightness.light, | ||||
|   ), | ||||
|  | ||||
|  | ||||
|   textButtonTheme: const TextButtonThemeData( | ||||
|     style: ButtonStyle( | ||||
|       foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR), | ||||
|       side: WidgetStatePropertyAll( | ||||
|         BorderSide( | ||||
|           color: PRIMARY_COLOR, | ||||
|           width: 1, | ||||
|         ), | ||||
|       ), | ||||
|     ) | ||||
|     ), | ||||
|  | ||||
|   elevatedButtonTheme: const ElevatedButtonThemeData( | ||||
|     style: ButtonStyle( | ||||
|       foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR), | ||||
|     ) | ||||
|   ), | ||||
|  | ||||
|   outlinedButtonTheme: const OutlinedButtonThemeData( | ||||
|     style: ButtonStyle( | ||||
|       foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR), | ||||
|     ) | ||||
|   ), | ||||
|  | ||||
|  | ||||
|   cardTheme: const CardTheme( | ||||
|     shadowColor: Colors.grey, | ||||
|     elevation: 2, | ||||
|     margin: EdgeInsets.all(10), | ||||
|   ), | ||||
|  | ||||
|   sliderTheme: const SliderThemeData( | ||||
|     trackHeight: 15, | ||||
|     inactiveTrackColor: Colors.grey, | ||||
|     thumbColor: PRIMARY_COLOR, | ||||
|     activeTrackColor: GRADIENT_END | ||||
|   ) | ||||
| ); | ||||
|  | ||||
|  | ||||
| const Gradient APP_GRADIENT = LinearGradient( | ||||
|   begin: Alignment.topLeft, | ||||
|   end: Alignment.bottomRight, | ||||
|   colors: [GRADIENT_START, GRADIENT_END], | ||||
| ); | ||||
| @@ -1,4 +1,3 @@ | ||||
| import 'package:anyway/pages/settings.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
| import 'package:anyway/constants.dart'; | ||||
| @@ -7,9 +6,10 @@ import 'package:anyway/structs/trip.dart'; | ||||
| import 'package:anyway/modules/trips_saved_list.dart'; | ||||
| import 'package:anyway/utils/load_trips.dart'; | ||||
|  | ||||
| import 'package:anyway/pages/new_trip_location.dart'; | ||||
| import 'package:anyway/pages/new_trip.dart'; | ||||
| import 'package:anyway/pages/current_trip.dart'; | ||||
| import 'package:anyway/pages/onboarding.dart'; | ||||
| import 'package:anyway/pages/profile.dart'; | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -62,7 +62,7 @@ class _BasePageState extends State<BasePage> { | ||||
|                         ) | ||||
|                       ); | ||||
|                     }, | ||||
|                     label: Text("Plan a trip"), | ||||
|                     label: Text("Plan a trip now"), | ||||
|                   ), | ||||
|                 ); | ||||
|               } | ||||
| @@ -74,33 +74,33 @@ class _BasePageState extends State<BasePage> { | ||||
|       } | ||||
|     } else if (widget.mainScreen == "tutorial") { | ||||
|       currentView = OnboardingPage(); | ||||
|     } else if (widget.mainScreen == "settings") { | ||||
|       currentView = SettingsPage(); | ||||
|     } else if (widget.mainScreen == "profile") { | ||||
|       currentView = ProfilePage(); | ||||
|     } | ||||
|  | ||||
|     final ThemeData theme = Theme.of(context); | ||||
|      | ||||
|     return Scaffold( | ||||
|       appBar: AppBar(title: Text(APP_NAME)), | ||||
|       body: Center(child: currentView), | ||||
|       drawer: Drawer( | ||||
|         child: Column( | ||||
|           children: [ | ||||
|             Container( | ||||
|             DrawerHeader( | ||||
|               decoration: BoxDecoration( | ||||
|                 gradient: APP_GRADIENT, | ||||
|                 gradient: LinearGradient(colors: [Colors.red, Colors.yellow]) | ||||
|               ), | ||||
|               height: 150, | ||||
|               child: Center( | ||||
|                 child: Text( | ||||
|                   APP_NAME, | ||||
|                   style: TextStyle( | ||||
|                     color: Colors.white, | ||||
|                     color: Colors.grey[800], | ||||
|                     fontSize: 24, | ||||
|                     fontWeight: FontWeight.bold, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|  | ||||
|             ListTile( | ||||
|               title: const Text('Your Trips'), | ||||
|               leading: const Icon(Icons.map), | ||||
| @@ -130,7 +130,7 @@ class _BasePageState extends State<BasePage> { | ||||
|               }, | ||||
|               child: const Text('Clear trips'), | ||||
|             ), | ||||
|             const Divider(indent: 10, endIndent: 10), | ||||
|             const Divider(), | ||||
|             ListTile( | ||||
|               title: const Text('How to use'), | ||||
|               leading: Icon(Icons.help), | ||||
| @@ -148,11 +148,11 @@ class _BasePageState extends State<BasePage> { | ||||
|             ListTile( | ||||
|               title: const Text('Settings'), | ||||
|               leading: const Icon(Icons.settings), | ||||
|               selected: widget.mainScreen == "settings", | ||||
|               selected: widget.mainScreen == "profile", | ||||
|               onTap: () { | ||||
|                 Navigator.of(context).push( | ||||
|                   MaterialPageRoute( | ||||
|                     builder: (context) => BasePage(mainScreen: "settings") | ||||
|                     builder: (context) => BasePage(mainScreen: "profile") | ||||
|                   ) | ||||
|                 ); | ||||
|               }, | ||||
|   | ||||
| @@ -15,7 +15,7 @@ class App extends StatelessWidget { | ||||
|     return MaterialApp( | ||||
|       title: APP_NAME, | ||||
|       home: BasePage(mainScreen: "map"), | ||||
|       theme: APP_THEME, | ||||
|       theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.red[600]), | ||||
|       scaffoldMessengerKey: rootScaffoldMessengerKey | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -1,37 +0,0 @@ | ||||
| import 'package:anyway/structs/trip.dart'; | ||||
| import 'package:auto_size_text/auto_size_text.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
| class CurrentTripErrorMessage extends StatefulWidget { | ||||
|   final Trip trip; | ||||
|   const CurrentTripErrorMessage({ | ||||
|     super.key, | ||||
|     required this.trip, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   State<CurrentTripErrorMessage> createState() => _CurrentTripErrorMessageState(); | ||||
| } | ||||
|  | ||||
| class _CurrentTripErrorMessageState extends State<CurrentTripErrorMessage> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) => Center( | ||||
|     child: Row( | ||||
|       mainAxisAlignment: MainAxisAlignment.center, | ||||
|       children: [ | ||||
|         const Icon( | ||||
|           Icons.error_outline, | ||||
|           color: Colors.red, | ||||
|           size: 50, | ||||
|         ), | ||||
|         const Padding( | ||||
|           padding: EdgeInsets.only(left: 10), | ||||
|         ), | ||||
|         AutoSizeText( | ||||
|           'Error: ${widget.trip.errorDescription}', | ||||
|           maxLines: 3, | ||||
|         ), | ||||
|       ], | ||||
|     ) | ||||
|   ); | ||||
| } | ||||
| @@ -1,50 +1,122 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'dart:developer'; | ||||
|  | ||||
| import 'package:anyway/structs/trip.dart'; | ||||
| import 'package:auto_size_text/auto_size_text.dart'; | ||||
|  | ||||
| import 'package:anyway/pages/current_trip.dart'; | ||||
| import 'package:anyway/structs/trip.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
|  | ||||
| class CurrentTripGreeter extends StatefulWidget { | ||||
| class Greeter extends StatefulWidget { | ||||
|   final Trip trip; | ||||
|  | ||||
|   CurrentTripGreeter({ | ||||
|     super.key, | ||||
|   Greeter({ | ||||
|     required this.trip, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   State<CurrentTripGreeter> createState() => _CurrentTripGreeterState(); | ||||
|   State<Greeter> createState() => _GreeterState(); | ||||
| } | ||||
|  | ||||
|  | ||||
| class _CurrentTripGreeterState extends State<CurrentTripGreeter> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) => Center( | ||||
|     child: FutureBuilder( | ||||
|       future: widget.trip.cityName, | ||||
|       builder: (BuildContext context, AsyncSnapshot<String> snapshot) { | ||||
|         if (snapshot.hasData) { | ||||
|           return AutoSizeText( | ||||
|             maxLines: 1, | ||||
|             'Welcome to ${snapshot.data}!', | ||||
|             style: greeterStyle | ||||
|           ); | ||||
|         } else if (snapshot.hasError) { | ||||
|           return AutoSizeText( | ||||
|             maxLines: 1, | ||||
|             'Welcome to your trip!', | ||||
|             style: greeterStyle | ||||
|           ); | ||||
|         } else { | ||||
|           return AutoSizeText( | ||||
|             maxLines: 1, | ||||
|             'Welcome to ...', | ||||
|             style: greeterStyle | ||||
|           ); | ||||
| class _GreeterState extends State<Greeter> { | ||||
|    | ||||
|   Widget greeterBuilder (BuildContext context, Widget? child) { | ||||
|     ThemeData theme = Theme.of(context); | ||||
|     TextStyle greeterStyle = TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24); | ||||
|  | ||||
|     Widget topGreeter; | ||||
|  | ||||
|     if (widget.trip.uuid != 'pending') { | ||||
|       topGreeter = FutureBuilder( | ||||
|         future: widget.trip.cityName, | ||||
|         builder: (BuildContext context, AsyncSnapshot<String> snapshot) { | ||||
|           if (snapshot.hasData) { | ||||
|             return AutoSizeText( | ||||
|               maxLines: 1, | ||||
|               'Welcome to ${snapshot.data}!', | ||||
|               style: greeterStyle | ||||
|             ); | ||||
|           } else if (snapshot.hasError) { | ||||
|             log('Error while fetching city name'); | ||||
|             return AutoSizeText( | ||||
|               maxLines: 1, | ||||
|               'Welcome to your trip!', | ||||
|               style: greeterStyle | ||||
|             ); | ||||
|           } else { | ||||
|             return AutoSizeText( | ||||
|               maxLines: 1, | ||||
|               'Welcome to ...', | ||||
|               style: greeterStyle | ||||
|             ); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     ) | ||||
|       ); | ||||
|     } else { | ||||
|       // still awaiting the trip | ||||
|       // We can hopefully infer the city name from the cityName future | ||||
|       // Show a linear loader at the bottom and an info message above | ||||
|       topGreeter = Column( | ||||
|         mainAxisAlignment: MainAxisAlignment.end, | ||||
|         children: [ | ||||
|           FutureBuilder( | ||||
|             future: widget.trip.cityName, | ||||
|             builder: (BuildContext context, AsyncSnapshot<String> snapshot) { | ||||
|               if (snapshot.hasData) { | ||||
|                 return AutoSizeText( | ||||
|                   maxLines: 1, | ||||
|                   'Generating your trip to ${snapshot.data}...', | ||||
|                   style: greeterStyle | ||||
|                 ); | ||||
|               } else if (snapshot.hasError) { | ||||
|                 // the exact error is shown in the central part of the trip overview. No need to show it here | ||||
|                 return AutoSizeText( | ||||
|                   maxLines: 1, | ||||
|                   'Error while loading trip.', | ||||
|                   style: greeterStyle | ||||
|                   ); | ||||
|               } | ||||
|               return AutoSizeText( | ||||
|                   maxLines: 1, | ||||
|                   'Generating your trip...', | ||||
|                   style: greeterStyle | ||||
|                   ); | ||||
|             } | ||||
|           ), | ||||
|           Padding( | ||||
|             padding: EdgeInsets.all(5), | ||||
|             child: const LinearProgressIndicator() | ||||
|           ) | ||||
|         ] | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return Center( | ||||
|       child: Column( | ||||
|         children: [ | ||||
|           // Padding(padding: EdgeInsets.only(top: 20)), | ||||
|           topGreeter, | ||||
|           Padding( | ||||
|             padding: EdgeInsets.all(20), | ||||
|             child: bottomGreeter | ||||
|           ), | ||||
|         ], | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget bottomGreeter = const Text( | ||||
|     "Busy day ahead? Here is how to make the most of it!", | ||||
|     style: TextStyle(color: Colors.black, fontSize: 18), | ||||
|     textAlign: TextAlign.center, | ||||
|   ); | ||||
|  | ||||
|  | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return ListenableBuilder( | ||||
|       listenable: widget.trip, | ||||
|       builder: greeterBuilder, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -16,7 +16,7 @@ List<Widget> landmarksList(Trip trip) { | ||||
|  | ||||
|   log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks"); | ||||
|  | ||||
|   if (trip.landmarks.isEmpty || trip.landmarks.length <= 1 && trip.landmarks.first.type == typeStart ) { | ||||
|   if (trip.landmarks.isEmpty || trip.landmarks.length <= 1 && trip.landmarks.first.type == start ) { | ||||
|     children.add( | ||||
|       const Text("No landmarks in this trip"), | ||||
|     ); | ||||
| @@ -32,6 +32,7 @@ List<Widget> landmarksList(Trip trip) { | ||||
|         onDismissed: (direction) { | ||||
|           log('Removing ${landmark.name}'); | ||||
|           trip.removeLandmark(landmark); | ||||
|           // Then show a snackbar | ||||
|  | ||||
|           rootScaffoldMessengerKey.currentState!.showSnackBar( | ||||
|             SnackBar(content: Text("We won't show ${landmark.name} again")) | ||||
|   | ||||
| @@ -1,60 +0,0 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:auto_size_text/auto_size_text.dart'; | ||||
|  | ||||
| import 'package:anyway/structs/trip.dart'; | ||||
| import 'package:anyway/pages/current_trip.dart'; | ||||
|  | ||||
| class CurrentTripLoadingIndicator extends StatefulWidget { | ||||
|   final Trip trip; | ||||
|   const CurrentTripLoadingIndicator({ | ||||
|     super.key, | ||||
|     required this.trip, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   State<CurrentTripLoadingIndicator> createState() => _CurrentTripLoadingIndicatorState(); | ||||
| } | ||||
|  | ||||
| class _CurrentTripLoadingIndicatorState extends State<CurrentTripLoadingIndicator> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) => Center( | ||||
|     child: FutureBuilder( | ||||
|       future: widget.trip.cityName, | ||||
|       builder: (BuildContext context, AsyncSnapshot<String> snapshot) { | ||||
|         Widget greeter; | ||||
|         Widget loadingIndicator = const Padding( | ||||
|           padding: EdgeInsets.only(top: 10), | ||||
|           child: CircularProgressIndicator() | ||||
|         ); | ||||
|  | ||||
|         if (snapshot.hasData) { | ||||
|           greeter = AutoSizeText( | ||||
|             maxLines: 1, | ||||
|             'Generating your trip to ${snapshot.data}...', | ||||
|             style: greeterStyle, | ||||
|           ); | ||||
|         } else if (snapshot.hasError) { | ||||
|           // the exact error is shown in the central part of the trip overview. No need to show it here | ||||
|           greeter = AutoSizeText( | ||||
|             maxLines: 1, | ||||
|             'Error while loading trip.', | ||||
|             style: greeterStyle, | ||||
|             ); | ||||
|         } else { | ||||
|           greeter = AutoSizeText( | ||||
|             maxLines: 1, | ||||
|             'Generating your trip...', | ||||
|             style: greeterStyle, | ||||
|             ); | ||||
|         } | ||||
|         return Column( | ||||
|           mainAxisAlignment: MainAxisAlignment.center, | ||||
|           children: [ | ||||
|             greeter, | ||||
|             loadingIndicator, | ||||
|           ], | ||||
|         ); | ||||
|       } | ||||
|     ) | ||||
|   ); | ||||
| } | ||||
| @@ -1,28 +1,27 @@ | ||||
| import 'dart:collection'; | ||||
|  | ||||
| import 'package:anyway/constants.dart'; | ||||
| import 'package:anyway/modules/landmark_map_marker.dart'; | ||||
| import 'package:anyway/modules/themed_marker.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:anyway/structs/landmark.dart'; | ||||
| import 'package:anyway/structs/trip.dart'; | ||||
| import 'package:google_maps_flutter/google_maps_flutter.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
| import 'package:widget_to_marker/widget_to_marker.dart'; | ||||
|  | ||||
|  | ||||
| class CurrentTripMap extends StatefulWidget { | ||||
| class MapWidget extends StatefulWidget { | ||||
|  | ||||
|   final Trip? trip; | ||||
|  | ||||
|   CurrentTripMap({ | ||||
|   MapWidget({ | ||||
|     this.trip | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   State<CurrentTripMap> createState() => _CurrentTripMapState(); | ||||
|   State<MapWidget> createState() => _MapWidgetState(); | ||||
| } | ||||
|  | ||||
| class _CurrentTripMapState extends State<CurrentTripMap> { | ||||
| class _MapWidgetState extends State<MapWidget> { | ||||
|   late GoogleMapController mapController; | ||||
|  | ||||
|   CameraPosition _cameraPosition = CameraPosition( | ||||
| @@ -68,27 +67,9 @@ class _CurrentTripMapState extends State<CurrentTripMap> { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     widget.trip?.addListener(setMapMarkers); | ||||
|     Future<SharedPreferences> preferences = SharedPreferences.getInstance(); | ||||
|  | ||||
|     return FutureBuilder( | ||||
|       future: preferences, | ||||
|       builder: (context, snapshot) { | ||||
|         if (snapshot.hasData) { | ||||
|           SharedPreferences prefs = snapshot.data as SharedPreferences; | ||||
|           bool useLocation = prefs.getBool('useLocation') ?? true; | ||||
|           return _buildMap(useLocation); | ||||
|         } else { | ||||
|           return const CircularProgressIndicator(); | ||||
|         } | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildMap(bool useLocation) { | ||||
|     return GoogleMap( | ||||
|       onMapCreated: _onMapCreated, | ||||
|       initialCameraPosition: _cameraPosition, | ||||
| @@ -98,9 +79,7 @@ class _CurrentTripMapState extends State<CurrentTripMap> { | ||||
|       cloudMapId: MAP_ID, | ||||
|       mapToolbarEnabled: false, | ||||
|       zoomControlsEnabled: false, | ||||
|       myLocationEnabled: useLocation, | ||||
|       myLocationButtonEnabled: false, | ||||
|  | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,82 +0,0 @@ | ||||
| import 'package:anyway/constants.dart'; | ||||
| import 'package:anyway/modules/current_trip_error_message.dart'; | ||||
| import 'package:anyway/modules/current_trip_loading_indicator.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
| import 'package:anyway/structs/trip.dart'; | ||||
| import 'package:anyway/modules/current_trip_summary.dart'; | ||||
| import 'package:anyway/modules/current_trip_save_button.dart'; | ||||
| import 'package:anyway/modules/current_trip_landmarks_list.dart'; | ||||
| import 'package:anyway/modules/current_trip_greeter.dart'; | ||||
|  | ||||
|  | ||||
| class CurrentTripPanel extends StatefulWidget { | ||||
|   final ScrollController controller; | ||||
|   final Trip trip; | ||||
|  | ||||
|   const CurrentTripPanel({ | ||||
|     super.key, | ||||
|     required this.controller, | ||||
|     required this.trip, | ||||
|     }); | ||||
|  | ||||
|   @override | ||||
|   State<CurrentTripPanel> createState() => _CurrentTripPanelState(); | ||||
| } | ||||
|  | ||||
| class _CurrentTripPanelState extends State<CurrentTripPanel> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return ListenableBuilder( | ||||
|       listenable: widget.trip, | ||||
|       builder: (context, child) { | ||||
|         if (widget.trip.uuid == 'error') { | ||||
|           return Align( | ||||
|               alignment: Alignment.topCenter, | ||||
|               child: SizedBox( | ||||
|                 // reuse the exact same height as the panel has when collapsed | ||||
|                 // this way the greeter will be centered when the panel is collapsed | ||||
|                 height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 20, | ||||
|                 child: CurrentTripErrorMessage(trip: widget.trip) | ||||
|               ), | ||||
|             ); | ||||
|         } else if (widget.trip.uuid == 'pending') { | ||||
|             return Align( | ||||
|               alignment: Alignment.topCenter, | ||||
|               child: SizedBox( | ||||
|                 // reuse the exact same height as the panel has when collapsed | ||||
|                 // this way the greeter will be centered when the panel is collapsed | ||||
|                 height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 20, | ||||
|                 child: CurrentTripLoadingIndicator(trip: widget.trip), | ||||
|               ), | ||||
|             ); | ||||
|         } else { | ||||
|           return ListView( | ||||
|             controller: widget.controller, | ||||
|             padding: const EdgeInsets.only(bottom: 30), | ||||
|             children: [ | ||||
|               SizedBox( | ||||
|                 // reuse the exact same height as the panel has when collapsed | ||||
|                 // this way the greeter will be centered when the panel is collapsed | ||||
|                 height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 20, | ||||
|                 child: CurrentTripGreeter(trip: widget.trip), | ||||
|               ), | ||||
|  | ||||
|               const Padding(padding: EdgeInsets.only(top: 10)), | ||||
|  | ||||
|               // CurrentTripSummary(trip: widget.trip), | ||||
|  | ||||
|               // const Divider(), | ||||
|  | ||||
|               ...landmarksList(widget.trip), | ||||
|  | ||||
|               const Padding(padding: EdgeInsets.only(top: 10)), | ||||
|  | ||||
|               Center(child: saveButton(widget.trip)), | ||||
|             ], | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
| }   | ||||
| @@ -1,5 +1,4 @@ | ||||
|  | ||||
| import 'package:anyway/main.dart'; | ||||
| import 'package:anyway/structs/trip.dart'; | ||||
| import 'package:auto_size_text/auto_size_text.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| @@ -9,13 +8,6 @@ Widget saveButton(Trip trip) => ElevatedButton( | ||||
|   onPressed: () async { | ||||
|     SharedPreferences prefs = await SharedPreferences.getInstance(); | ||||
|     trip.toPrefs(prefs); | ||||
|     rootScaffoldMessengerKey.currentState!.showSnackBar( | ||||
|       SnackBar( | ||||
|         content: Text('Trip saved'), | ||||
|         duration: Duration(seconds: 2), | ||||
|         dismissDirection: DismissDirection.horizontal | ||||
|       ) | ||||
|     ); | ||||
|   }, | ||||
|   child: SizedBox( | ||||
|     width: 100, | ||||
|   | ||||
| @@ -1,31 +0,0 @@ | ||||
| import 'package:anyway/structs/trip.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
| class CurrentTripSummary extends StatefulWidget { | ||||
|   final Trip trip; | ||||
|   const CurrentTripSummary({ | ||||
|     super.key, | ||||
|     required this.trip, | ||||
|     }); | ||||
|  | ||||
|   @override | ||||
|   State<CurrentTripSummary> createState() => _CurrentTripSummaryState(); | ||||
| } | ||||
|  | ||||
| class _CurrentTripSummaryState extends State<CurrentTripSummary> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Column( | ||||
|       children: [ | ||||
|         Text('Summary'), | ||||
|         // Text('Start: ${widget.trip.start}'), | ||||
|         // Text('End: ${widget.trip.end}'), | ||||
|         Text('Total duration: ${widget.trip.totalTime}'), | ||||
|         Text('Total distance: ${widget.trip.totalTime}'), | ||||
|         // Text('Fuel: ${widget.trip.fuel}'), | ||||
|         // Text('Cost: ${widget.trip.cost}'), | ||||
|       ], | ||||
|     ); | ||||
|      | ||||
|   } | ||||
| } | ||||
| @@ -18,6 +18,10 @@ class _LandmarkCardState extends State<LandmarkCard> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     ThemeData theme = Theme.of(context); | ||||
|     ButtonStyle buttonStyle = TextButton.styleFrom( | ||||
|       backgroundColor: Colors.orange, | ||||
|       fixedSize: Size.fromHeight(20) | ||||
|     ); | ||||
|     return Container( | ||||
|       height: 160, | ||||
|       child: Card( | ||||
| @@ -36,7 +40,7 @@ class _LandmarkCardState extends State<LandmarkCard> { | ||||
|               width: 160, | ||||
|               child: CachedNetworkImage( | ||||
|                 imageUrl: widget.landmark.imageURL ?? '', | ||||
|                 placeholder: (context, url) => Center(child: CircularProgressIndicator()), | ||||
|                 placeholder: (context, url) => CircularProgressIndicator(), | ||||
|                 errorWidget: (context, error, stackTrace) => Icon(Icons.question_mark_outlined), | ||||
|                 // TODO: make this a switch statement to load a placeholder if null | ||||
|                 // cover the whole container meaning the image will be cropped | ||||
| @@ -84,18 +88,21 @@ class _LandmarkCardState extends State<LandmarkCard> { | ||||
|                         // show the type, the website, and the wikipedia link as buttons/labels in a row | ||||
|                         children: [ | ||||
|                           TextButton.icon( | ||||
|                             style: buttonStyle, | ||||
|                             onPressed: () {}, | ||||
|                             icon: widget.landmark.type.icon, | ||||
|                             label: Text(widget.landmark.type.name), | ||||
|                           ), | ||||
|                           if (widget.landmark.duration != null && widget.landmark.duration!.inMinutes > 0) | ||||
|                             TextButton.icon( | ||||
|                               style: buttonStyle, | ||||
|                               onPressed: () {}, | ||||
|                               icon: Icon(Icons.hourglass_bottom), | ||||
|                               label: Text('${widget.landmark.duration!.inMinutes} minutes'), | ||||
|                             ), | ||||
|                           if (widget.landmark.websiteURL != null) | ||||
|                             TextButton.icon( | ||||
|                               style: buttonStyle, | ||||
|                               onPressed: () async { | ||||
|                                 // open a browser with the website link | ||||
|                                 await launchUrl(Uri.parse(widget.landmark.websiteURL!)); | ||||
| @@ -105,6 +112,7 @@ class _LandmarkCardState extends State<LandmarkCard> { | ||||
|                             ), | ||||
|                           if (widget.landmark.wikipediaURL != null) | ||||
|                             TextButton.icon( | ||||
|                               style: buttonStyle, | ||||
|                               onPressed: () async { | ||||
|                                 // open a browser with the wikipedia link | ||||
|                                 await launchUrl(Uri.parse(widget.landmark.wikipediaURL!)); | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import 'package:anyway/layout.dart'; | ||||
| import 'package:anyway/main.dart'; | ||||
| import 'package:anyway/structs/preferences.dart'; | ||||
| import 'package:anyway/structs/trip.dart'; | ||||
| import 'package:anyway/utils/fetch_trip.dart'; | ||||
| @@ -9,12 +8,8 @@ import 'package:flutter/material.dart'; | ||||
|  | ||||
| class NewTripButton extends StatefulWidget { | ||||
|   final Trip trip; | ||||
|   final UserPreferences preferences; | ||||
|  | ||||
|   const NewTripButton({ | ||||
|     required this.trip, | ||||
|     required this.preferences, | ||||
|     }); | ||||
|   const NewTripButton({required this.trip}); | ||||
|  | ||||
|   @override | ||||
|   State<NewTripButton> createState() => _NewTripButtonState(); | ||||
| @@ -28,39 +23,42 @@ class _NewTripButtonState extends State<NewTripButton> { | ||||
|       listenable: widget.trip, | ||||
|       builder: (BuildContext context, Widget? child) { | ||||
|         if (widget.trip.landmarks.isEmpty){ | ||||
|           // Fallback if the trip setup is lagging behind | ||||
|           // This should in theory never happen | ||||
|           return Container(); | ||||
|         } | ||||
|         return FloatingActionButton.extended( | ||||
|           onPressed: onPressed, | ||||
|           icon: const Icon(Icons.directions), | ||||
|           label: AutoSizeText('Start planning!'), | ||||
|         );  | ||||
|       } | ||||
|           onPressed: () async { | ||||
|             Future<UserPreferences> preferences = loadUserPreferences(); | ||||
|             Trip trip = widget.trip; | ||||
|             fetchTrip(trip, preferences); | ||||
|             Navigator.of(context).push( | ||||
|               MaterialPageRoute( | ||||
|                 builder: (context) => BasePage(mainScreen: "map", trip: trip) | ||||
|               ) | ||||
|             ); | ||||
|           }, | ||||
|           icon: Icon(Icons.add), | ||||
|           label: FutureBuilder( | ||||
|             future: widget.trip.cityName, | ||||
|             builder: (context, snapshot) { | ||||
|               if (snapshot.connectionState == ConnectionState.done) { | ||||
|                 return AutoSizeText( | ||||
|                   'New trip to ${snapshot.data.toString()}', | ||||
|                   style: TextStyle(fontSize: 18), | ||||
|                   maxLines: 2, | ||||
|                 ); | ||||
|               } else { | ||||
|                 return AutoSizeText( | ||||
|                   'New trip to ...', | ||||
|                   style: TextStyle(fontSize: 18), | ||||
|                   maxLines: 2, | ||||
|                 ); | ||||
|               } | ||||
|             }, | ||||
|           ) | ||||
|         ); | ||||
|          | ||||
|       }  | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void onPressed() async { | ||||
|     // Check that the preferences are valid | ||||
|     UserPreferences preferences = widget.preferences; | ||||
|     if (preferences.nature.value == 0 && preferences.shopping.value == 0 && preferences.sightseeing.value == 0){ | ||||
|       rootScaffoldMessengerKey.currentState!.showSnackBar( | ||||
|         SnackBar(content: Text("Please specify at least one preference")) | ||||
|       ); | ||||
|     } else if (preferences.maxTime.value == 0){ | ||||
|       rootScaffoldMessengerKey.currentState!.showSnackBar( | ||||
|         SnackBar(content: Text("Please choose a longer duration")) | ||||
|       ); | ||||
|     } else { | ||||
|       Trip trip = widget.trip; | ||||
|       fetchTrip(trip, widget.preferences); | ||||
|       Navigator.of(context).push( | ||||
|         MaterialPageRoute( | ||||
|           builder: (context) => BasePage(mainScreen: "map", trip: trip) | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -6,13 +6,9 @@ import 'dart:developer'; | ||||
|  | ||||
| import 'package:anyway/structs/trip.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:geolocator/geolocator.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
|  | ||||
| class NewTripLocationSearch extends StatefulWidget { | ||||
|   Future<SharedPreferences> prefs = SharedPreferences.getInstance(); | ||||
|   Trip trip; | ||||
|    | ||||
|   NewTripLocationSearch( | ||||
|     this.trip, | ||||
|   ); | ||||
| @@ -43,66 +39,26 @@ class _NewTripLocationSearchState extends State<NewTripLocationSearch> { | ||||
|           uuid: 'pending', | ||||
|           name: query, | ||||
|           location: [location.latitude, location.longitude], | ||||
|           type: typeStart | ||||
|           type: start | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   late Widget locationSearchBar = SearchBar( | ||||
|     hintText: 'Enter a city name or long press on the map.', | ||||
|     onSubmitted: setTripLocation, | ||||
|     controller: _controller, | ||||
|     leading: Icon(Icons.search), | ||||
|     trailing: [ | ||||
|       ElevatedButton( | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return SearchBar( | ||||
|       hintText: 'Enter a city name or long press on the map.', | ||||
|       onSubmitted: setTripLocation, | ||||
|       controller: _controller, | ||||
|       leading: Icon(Icons.search), | ||||
|       trailing: [ElevatedButton( | ||||
|         onPressed: () { | ||||
|           setTripLocation(_controller.text); | ||||
|         }, | ||||
|         child: Text('Search'), | ||||
|       ) | ||||
|     ] | ||||
|   ); | ||||
|       ),] | ||||
|  | ||||
|  | ||||
|   late Widget useCurrentLocationButton = ElevatedButton( | ||||
|     onPressed: () async { | ||||
|       // this widget is only shown if the user has already granted location permissions | ||||
|       Position position = await Geolocator.getCurrentPosition(); | ||||
|       widget.trip.landmarks.clear(); | ||||
|       widget.trip.addLandmark( | ||||
|         Landmark( | ||||
|           uuid: 'pending', | ||||
|           name: 'start', | ||||
|           location: [position.latitude, position.longitude], | ||||
|           type: typeStart | ||||
|         ) | ||||
|       ); | ||||
|     }, | ||||
|     child: Text('Use current location'), | ||||
|   ); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return FutureBuilder( | ||||
|       future: widget.prefs, | ||||
|       builder: (context, snapshot) { | ||||
|         if (snapshot.hasData) { | ||||
|           final useLocation = snapshot.data!.getBool('useLocation') ?? false; | ||||
|           if (useLocation) { | ||||
|             return Column( | ||||
|               children: [ | ||||
|                 locationSearchBar, | ||||
|                 useCurrentLocationButton, | ||||
|               ], | ||||
|             ); | ||||
|           } else { | ||||
|             return locationSearchBar; | ||||
|           } | ||||
|         } else { | ||||
|           return locationSearchBar; | ||||
|         } | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| } | ||||
| @@ -1,13 +1,13 @@ | ||||
|  | ||||
| // A map that allows the user to select a location for a new trip. | ||||
| import 'dart:developer'; | ||||
|  | ||||
| import 'package:anyway/constants.dart'; | ||||
| import 'package:anyway/modules/landmark_map_marker.dart'; | ||||
| import 'package:anyway/modules/themed_marker.dart'; | ||||
| import 'package:anyway/structs/landmark.dart'; | ||||
| import 'package:anyway/structs/trip.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:google_maps_flutter/google_maps_flutter.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
| import 'package:widget_to_marker/widget_to_marker.dart'; | ||||
|  | ||||
|  | ||||
| @@ -22,7 +22,7 @@ class NewTripMap extends StatefulWidget { | ||||
| } | ||||
|  | ||||
| class _NewTripMapState extends State<NewTripMap> { | ||||
|   final CameraPosition _cameraPosition = const CameraPosition( | ||||
|   final CameraPosition _cameraPosition = CameraPosition( | ||||
|     target: LatLng(48.8566, 2.3522), | ||||
|     zoom: 11.0, | ||||
|   ); | ||||
| @@ -37,7 +37,7 @@ class _NewTripMapState extends State<NewTripMap> { | ||||
|         uuid: 'pending', | ||||
|         name: 'start', | ||||
|         location: [location.latitude, location.longitude], | ||||
|         type: typeStart | ||||
|         type: start | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
| @@ -70,26 +70,10 @@ class _NewTripMapState extends State<NewTripMap> { | ||||
|   } | ||||
|  | ||||
|  | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     widget.trip.addListener(updateTripDetails); | ||||
|     Future<SharedPreferences> preferences = SharedPreferences.getInstance(); | ||||
|  | ||||
|     return FutureBuilder( | ||||
|       future: preferences, | ||||
|       builder: (context, snapshot) { | ||||
|         if (snapshot.hasData) { | ||||
|           SharedPreferences prefs = snapshot.data as SharedPreferences; | ||||
|           bool useLocation = prefs.getBool('useLocation') ?? true; | ||||
|           return _buildMap(useLocation); | ||||
|         } else { | ||||
|           return const CircularProgressIndicator(); | ||||
|         } | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildMap(bool useLocation) { | ||||
|     return GoogleMap( | ||||
|       onMapCreated: _onMapCreated, | ||||
|       initialCameraPosition: _cameraPosition, | ||||
| @@ -98,8 +82,6 @@ class _NewTripMapState extends State<NewTripMap> { | ||||
|       cloudMapId: MAP_ID, | ||||
|       mapToolbarEnabled: false, | ||||
|       zoomControlsEnabled: false, | ||||
|       myLocationButtonEnabled: false, | ||||
|       myLocationEnabled: useLocation, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,41 +0,0 @@ | ||||
| import 'package:anyway/pages/new_trip_preferences.dart'; | ||||
| import 'package:anyway/structs/trip.dart'; | ||||
| import 'package:auto_size_text/auto_size_text.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
|  | ||||
| class NewTripOptionsButton extends StatefulWidget { | ||||
|   final Trip trip; | ||||
|  | ||||
|   const NewTripOptionsButton({required this.trip}); | ||||
|  | ||||
|   @override | ||||
|   State<NewTripOptionsButton> createState() => _NewTripOptionsButtonState(); | ||||
| } | ||||
|  | ||||
| class _NewTripOptionsButtonState extends State<NewTripOptionsButton> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return ListenableBuilder( | ||||
|       listenable: widget.trip, | ||||
|       builder: (BuildContext context, Widget? child) { | ||||
|         if (widget.trip.landmarks.isEmpty){ | ||||
|           return Container(); | ||||
|         } | ||||
|         return FloatingActionButton.extended( | ||||
|           onPressed: () async { | ||||
|             Navigator.of(context).push( | ||||
|               MaterialPageRoute( | ||||
|                 builder: (context) => NewTripPreferencesPage(trip: widget.trip) | ||||
|               ) | ||||
|             ); | ||||
|           }, | ||||
|           icon: const Icon(Icons.add), | ||||
|           label: const AutoSizeText('Set preferences') | ||||
|         );  | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -16,23 +16,20 @@ class OnboardingCard extends StatelessWidget { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     Color baseColor = Theme.of(context).colorScheme.secondary; | ||||
|     Color baseColor = Theme.of(context).primaryColor; | ||||
|     // have a different color for each card, incrementing the hue | ||||
|     Color currentColor = baseColor.withAlpha(baseColor.alpha - index * 30); | ||||
|     return Container( | ||||
|       color: currentColor, | ||||
|       alignment: Alignment.center, | ||||
|       child: Padding( | ||||
|         padding: EdgeInsets.all(20), | ||||
|         child: Column( | ||||
|           mainAxisAlignment: MainAxisAlignment.center, | ||||
|           children: [ | ||||
|             Text( | ||||
|               title, | ||||
|               style: TextStyle( | ||||
|                 fontSize: 24, | ||||
|                 fontWeight: FontWeight.bold, | ||||
|                 color: Colors.white, | ||||
|               ), | ||||
|             ), | ||||
|             Padding(padding: EdgeInsets.only(top: 20)), | ||||
| @@ -47,8 +44,7 @@ class OnboardingCard extends StatelessWidget { | ||||
|                 fontSize: 16, | ||||
|               ), | ||||
|             ), | ||||
|  | ||||
|           ] | ||||
|           ], | ||||
|         ), | ||||
|       ) | ||||
|     ); | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import 'package:anyway/constants.dart'; | ||||
| import 'package:anyway/structs/landmark.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| 
 | ||||
| @@ -17,9 +16,21 @@ class ThemedMarker extends StatelessWidget { | ||||
|   Widget build(BuildContext context) { | ||||
|     // This returns an outlined circle, with an icon corresponding to the landmark type | ||||
|     // As a small dot, the number of the landmark is displayed in the top right | ||||
|     Icon icon; | ||||
|     if (landmark.type == sightseeing) { | ||||
|       icon = Icon(Icons.church, color: Colors.black, size: 50); | ||||
|     } else if (landmark.type == nature) { | ||||
|       icon = Icon(Icons.park, color: Colors.black, size: 50); | ||||
|     } else if (landmark.type == shopping) { | ||||
|       icon = Icon(Icons.shopping_cart, color: Colors.black, size: 50); | ||||
|     } else if (landmark.type == start || landmark.type == finish) { | ||||
|       icon = Icon(Icons.flag, color: Colors.black, size: 50); | ||||
|     } else { | ||||
|       icon = Icon(Icons.location_on, color: Colors.black, size: 50); | ||||
|     } | ||||
| 
 | ||||
|     Widget? positionIndicator; | ||||
|     if (landmark.type != typeStart && landmark.type != typeFinish) { | ||||
|     if (landmark.type != start && landmark.type != finish) { | ||||
|       positionIndicator = Positioned( | ||||
|         top: 0, | ||||
|         right: 0, | ||||
| @@ -40,14 +51,14 @@ class ThemedMarker extends StatelessWidget { | ||||
|         children: [ | ||||
|           Container( | ||||
|             decoration: BoxDecoration( | ||||
|               gradient: APP_GRADIENT, | ||||
|               gradient: LinearGradient( | ||||
|                 colors: [Colors.red, Colors.yellow] | ||||
|               ), | ||||
|               shape: BoxShape.circle, | ||||
|               border: Border.all(color: Colors.black, width: 5), | ||||
|             ), | ||||
|             width: 70, | ||||
|             height: 70, | ||||
|             padding: const EdgeInsets.all(5), | ||||
|             child: Icon(landmark.type.icon.icon, size: 50), | ||||
|             padding: EdgeInsets.all(5), | ||||
|             child: icon | ||||
|           ), | ||||
|           if (positionIndicator != null) positionIndicator, | ||||
|         ], | ||||
| @@ -1,17 +1,12 @@ | ||||
| import 'package:anyway/constants.dart'; | ||||
| import 'package:anyway/modules/current_trip_save_button.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:sliding_up_panel/sliding_up_panel.dart'; | ||||
|  | ||||
| import 'package:anyway/structs/trip.dart'; | ||||
| import 'package:anyway/modules/current_trip_landmarks_list.dart'; | ||||
| import 'package:anyway/modules/current_trip_greeter.dart'; | ||||
| import 'package:anyway/modules/current_trip_map.dart'; | ||||
| import 'package:anyway/modules/current_trip_panel.dart'; | ||||
|  | ||||
| final Shader textGradient = APP_GRADIENT.createShader(Rect.fromLTWH(0.0, 0.0, 200.0, 70.0)); | ||||
| TextStyle greeterStyle = TextStyle( | ||||
|   foreground: Paint()..shader = textGradient, | ||||
|   fontWeight: FontWeight.bold, | ||||
|   fontSize: 26 | ||||
| ); | ||||
|  | ||||
|  | ||||
| class TripPage extends StatefulWidget { | ||||
| @@ -32,21 +27,16 @@ class _TripPageState extends State<TripPage> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return SlidingUpPanel( | ||||
|         // use panelBuilder instead of panel so that we can reuse the scrollcontroller for the listview | ||||
|         panelBuilder: (scrollcontroller) => CurrentTripPanel(controller: scrollcontroller, trip: widget.trip), | ||||
|         // using collapsed and panelBuilder seems to show both at the same time, so we include the greeter in the panelBuilder | ||||
|         // collapsed: Greeter(trip: widget.trip), | ||||
|         body: CurrentTripMap(trip: widget.trip), | ||||
|         minHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT, | ||||
|         maxHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MAX_HEIGHT, | ||||
|         // padding in this context is annoying: it offsets the notion of vertical alignment. | ||||
|         // children that want to be centered vertically need to have their size adjusted by 2x the padding | ||||
|         padding: const EdgeInsets.all(10.0), | ||||
|         // Panel snapping should not be disabled because it significantly improves the user experience | ||||
|         // panelSnapping: false | ||||
|         borderRadius: const BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)), | ||||
|         parallaxEnabled: true, | ||||
|         boxShadow: const [ | ||||
|         panelBuilder: (sc) => _panelFull(sc), | ||||
|         // collapsed: _floatingCollapsed(), | ||||
|         body: MapWidget(trip: widget.trip), | ||||
|         // renderPanelSheet: false, | ||||
|         // backdropEnabled: true, | ||||
|         maxHeight: MediaQuery.of(context).size.height * 0.8, | ||||
|         padding: EdgeInsets.only(left: 10, right: 10, top: 25, bottom: 10), | ||||
|         // panelSnapping: false, | ||||
|         borderRadius: BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)), | ||||
|         boxShadow: [ | ||||
|           BoxShadow( | ||||
|             blurRadius: 20.0, | ||||
|             color: Colors.black, | ||||
| @@ -54,4 +44,41 @@ class _TripPageState extends State<TripPage> { | ||||
|         ], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|    | ||||
|   Widget _panelFull(ScrollController sc) { | ||||
|     return ListenableBuilder( | ||||
|       listenable: widget.trip, | ||||
|       builder: (context, child) { | ||||
|         if (widget.trip.uuid != 'pending' && widget.trip.uuid != 'error') { | ||||
|           return ListView( | ||||
|             controller: sc, | ||||
|             padding: EdgeInsets.only(bottom: 35), | ||||
|             children: [ | ||||
|               Greeter(trip: widget.trip), | ||||
|               ...landmarksList(widget.trip), | ||||
|               Padding(padding: EdgeInsets.only(top: 10)), | ||||
|               Center(child: saveButton(widget.trip)), | ||||
|             ], | ||||
|           ); | ||||
|         } else if(widget.trip.uuid == 'pending') { | ||||
|           return Greeter(trip: widget.trip); | ||||
|         } else { | ||||
|           return Column( | ||||
|             children: [ | ||||
|               const Icon( | ||||
|                 Icons.error_outline, | ||||
|                 color: Colors.red, | ||||
|                 size: 60, | ||||
|               ), | ||||
|               Padding( | ||||
|                 padding: const EdgeInsets.only(top: 16), | ||||
|                 child: Text('Error: ${widget.trip.errorDescription}'), | ||||
|               ), | ||||
|             ], | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,11 @@ | ||||
| import 'package:anyway/modules/new_trip_button.dart'; | ||||
| import 'package:anyway/modules/new_trip_options_button.dart'; | ||||
| import 'package:anyway/structs/landmark.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:geocoding/geocoding.dart'; | ||||
| 
 | ||||
| import 'package:anyway/layout.dart'; | ||||
| import 'package:anyway/utils/fetch_trip.dart'; | ||||
| import 'package:anyway/structs/preferences.dart'; | ||||
| import "package:anyway/structs/trip.dart"; | ||||
| import 'package:anyway/modules/new_trip_location_search.dart'; | ||||
| import 'package:anyway/modules/new_trip_map.dart'; | ||||
| @@ -15,6 +19,7 @@ class NewTripPage extends StatefulWidget { | ||||
| } | ||||
| 
 | ||||
| class _NewTripPageState extends State<NewTripPage> { | ||||
|   final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); | ||||
|   final TextEditingController latController = TextEditingController(); | ||||
|   final TextEditingController lonController = TextEditingController(); | ||||
|   Trip trip = Trip(); | ||||
| @@ -35,7 +40,7 @@ class _NewTripPageState extends State<NewTripPage> { | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|       floatingActionButton: NewTripOptionsButton(trip: trip), | ||||
|       floatingActionButton: NewTripButton(trip: trip), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,113 +0,0 @@ | ||||
| import 'package:anyway/modules/new_trip_button.dart'; | ||||
| import 'package:anyway/structs/preferences.dart'; | ||||
| import 'package:anyway/structs/trip.dart'; | ||||
| import 'package:flutter/cupertino.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
|  | ||||
|  | ||||
| class NewTripPreferencesPage extends StatefulWidget { | ||||
|   final Trip trip; | ||||
|   const NewTripPreferencesPage({required this.trip}); | ||||
|  | ||||
|   @override | ||||
|   _NewTripPreferencesPageState createState() => _NewTripPreferencesPageState(); | ||||
| } | ||||
|  | ||||
| class _NewTripPreferencesPageState extends State<NewTripPreferencesPage> { | ||||
|   UserPreferences preferences = UserPreferences(); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|       body: ListView( | ||||
|         children: [ | ||||
|           // Center( | ||||
|           //   child: CircleAvatar( | ||||
|           //     radius: 100, | ||||
|           //     child: Icon(Icons.person, size: 100), | ||||
|           //   ) | ||||
|           // ), | ||||
|           Padding(padding: EdgeInsets.only(top: 30)), | ||||
|           Center( | ||||
|             child: FutureBuilder( | ||||
|               future: widget.trip.cityName, | ||||
|               builder: (context, snapshot) => Text( | ||||
|                 'Your trip to ${snapshot.hasData ? snapshot.data! : "..."}', | ||||
|                 style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold) | ||||
|               ) | ||||
|             ) | ||||
|           ), | ||||
|  | ||||
|           Center( | ||||
|             child: Padding( | ||||
|             padding: EdgeInsets.only(left: 10, right: 10, top: 20, bottom: 0), | ||||
|               child: Text('Tell us about your ideal trip.', style: TextStyle(fontSize: 18)) | ||||
|             ), | ||||
|           ), | ||||
|  | ||||
|           Divider(indent: 25, endIndent: 25, height: 50), | ||||
|  | ||||
|           durationPicker(preferences.maxTime), | ||||
|  | ||||
|           preferenceSliders([preferences.sightseeing, preferences.shopping, preferences.nature]), | ||||
|         ] | ||||
|       ), | ||||
|       floatingActionButton: NewTripButton(trip: widget.trip, preferences: preferences), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|  | ||||
|   Widget durationPicker(SinglePreference maxTime) { | ||||
|     return Card( | ||||
|       margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0), | ||||
|       shadowColor: Colors.grey, | ||||
|       child: ListTile( | ||||
|         leading: preferences.maxTime.icon, | ||||
|         title: Text(preferences.maxTime.description), | ||||
|         subtitle: CupertinoTimerPicker( | ||||
|           mode: CupertinoTimerPickerMode.hm, | ||||
|           initialTimerDuration: Duration(minutes: 90), | ||||
|           minuteInterval: 15, | ||||
|           onTimerDurationChanged: (Duration newDuration) { | ||||
|             setState(() { | ||||
|               preferences.maxTime.value = newDuration.inMinutes; | ||||
|             }); | ||||
|           }, | ||||
|         ) | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|  | ||||
|   Widget preferenceSliders(List<SinglePreference> prefs) { | ||||
|     List<Card> sliders = []; | ||||
|       for (SinglePreference pref in prefs) { | ||||
|       sliders.add( | ||||
|         Card( | ||||
|           child: ListTile( | ||||
|             leading: pref.icon, | ||||
|             title: Text(pref.name), | ||||
|             subtitle: Slider( | ||||
|               value: pref.value.toDouble(), | ||||
|               min: pref.minVal.toDouble(), | ||||
|               max: pref.maxVal.toDouble(), | ||||
|               divisions: pref.maxVal - pref.minVal, | ||||
|               label: pref.value.toString(), | ||||
|               onChanged: (double newValue) { | ||||
|                 setState(() { | ||||
|                   pref.value = newValue.toInt(); | ||||
|                 }); | ||||
|               }, | ||||
|             ) | ||||
|           ), | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return Column( | ||||
|       children: sliders | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -1,5 +1,5 @@ | ||||
| import 'package:anyway/modules/onboarding_card.dart'; | ||||
| import 'package:anyway/pages/new_trip_location.dart'; | ||||
| import 'package:anyway/pages/new_trip.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
| class OnboardingPage extends StatefulWidget { | ||||
|   | ||||
							
								
								
									
										177
									
								
								frontend/lib/pages/profile.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,177 @@ | ||||
| import 'package:anyway/constants.dart'; | ||||
| import 'package:anyway/structs/preferences.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
|  | ||||
| bool debugMode = false; | ||||
|  | ||||
| class ProfilePage extends StatefulWidget { | ||||
|   @override | ||||
|   _ProfilePageState createState() => _ProfilePageState(); | ||||
| } | ||||
|  | ||||
| class _ProfilePageState extends State<ProfilePage> { | ||||
|   Future<UserPreferences> _prefs = loadUserPreferences(); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return ListView( | ||||
|       children: [ | ||||
|         // First a round, centered image | ||||
|         Center( | ||||
|           child: CircleAvatar( | ||||
|             radius: 100, | ||||
|             child: Icon(Icons.person, size: 100), | ||||
|           ) | ||||
|         ), | ||||
|         Center( | ||||
|           child: Text('Curious traveler', style: TextStyle(fontSize: 24)) | ||||
|         ), | ||||
|  | ||||
|         Divider(indent: 25, endIndent: 25, height: 50), | ||||
|  | ||||
|         Center( | ||||
|           child: Padding( | ||||
|            padding: EdgeInsets.only(left: 10, right: 10, top: 0, bottom: 10), | ||||
|             child: Text('For a tailored experience, please rate your discovery preferences.', style: TextStyle(fontSize: 18)) | ||||
|           ), | ||||
|         ), | ||||
|  | ||||
|         FutureBuilder(future: _prefs, builder: futureSliders), | ||||
|  | ||||
|         Divider(indent: 25, endIndent: 25, height: 50), | ||||
|  | ||||
|         privacyInfo(), | ||||
|  | ||||
|         debugButton(), | ||||
|       ] | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget debugButton() { | ||||
|     return Padding( | ||||
|       padding: EdgeInsets.only(top: 20), | ||||
|       child: Row( | ||||
|         children: [ | ||||
|           Text('Debug mode'), | ||||
|           Switch( | ||||
|             value: debugMode, | ||||
|             onChanged: (bool? newValue) { | ||||
|               setState(() { | ||||
|                 debugMode = newValue!; | ||||
|                 showDialog( | ||||
|                   context: context, | ||||
|                   builder: (BuildContext context) { | ||||
|                     return AlertDialog( | ||||
|                       title: Text('Debug mode - custom API'), | ||||
|                       content: TextField( | ||||
|                         decoration: InputDecoration( | ||||
|                           hintText: 'http://localhost:8000' | ||||
|                         ), | ||||
|                       onChanged: (value) { | ||||
|                         setState(() { | ||||
|                           API_URL_BASE = value; | ||||
|                         }); | ||||
|                       }, | ||||
|                     ), | ||||
|                     actions: [ | ||||
|                       TextButton( | ||||
|                         child: Text('OK'), | ||||
|                         onPressed: () { | ||||
|                           Navigator.of(context).pop(); | ||||
|                         }, | ||||
|                       ), | ||||
|                     ], | ||||
|                     ); | ||||
|                   } | ||||
|                 ); | ||||
|               }); | ||||
|             } | ||||
|           ) | ||||
|         ], | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|  | ||||
|   Widget futureSliders(BuildContext context, AsyncSnapshot<UserPreferences> snapshot) { | ||||
|     if (snapshot.connectionState == ConnectionState.done) { | ||||
|       UserPreferences prefs = snapshot.data!; | ||||
|  | ||||
|       return Column( | ||||
|         children: [ | ||||
|           PreferenceSliders(prefs: [prefs.maxTime, prefs.maxDetour]), | ||||
|           Divider(indent: 25, endIndent: 25, height: 50), | ||||
|           PreferenceSliders(prefs: [prefs.sightseeing, prefs.shopping, prefs.nature]) | ||||
|         ] | ||||
|       ); | ||||
|     } else { | ||||
|       return CircularProgressIndicator(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget privacyInfo() { | ||||
|     return Padding( | ||||
|       padding: EdgeInsets.only(top: 20), | ||||
|       child: Row( | ||||
|         children: [ | ||||
|           Text('Privacy policy is available at '), | ||||
|           TextButton.icon( | ||||
|             icon: Icon(Icons.info), | ||||
|             label: Text(PRIVACY_URL), | ||||
|             onPressed: () { | ||||
|             } | ||||
|           ) | ||||
|         ], | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| class PreferenceSliders extends StatefulWidget { | ||||
|   final List<SinglePreference> prefs; | ||||
|  | ||||
|   PreferenceSliders({required this.prefs}); | ||||
|  | ||||
|   @override | ||||
|   State<PreferenceSliders> createState() => _PreferenceSlidersState(); | ||||
| } | ||||
|  | ||||
|  | ||||
| class _PreferenceSlidersState extends State<PreferenceSliders> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     List<Card> sliders = []; | ||||
|       for (SinglePreference pref in widget.prefs) { | ||||
|       sliders.add( | ||||
|         Card( | ||||
|           child: ListTile( | ||||
|             leading: pref.icon, | ||||
|             title: Text(pref.name), | ||||
|             subtitle: Slider( | ||||
|               value: pref.value.toDouble(), | ||||
|               min: pref.minVal.toDouble(), | ||||
|               max: pref.maxVal.toDouble(), | ||||
|               divisions: pref.maxVal - pref.minVal, | ||||
|               label: pref.value.toString(), | ||||
|               onChanged: (double newValue) { | ||||
|                 setState(() { | ||||
|                   pref.value = newValue.toInt(); | ||||
|                   pref.save(); | ||||
|                 }); | ||||
|               }, | ||||
|             ) | ||||
|           ), | ||||
|           margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0), | ||||
|           shadowColor: Colors.grey, | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return Column( | ||||
|       children: sliders); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -1,186 +0,0 @@ | ||||
| import 'package:anyway/constants.dart'; | ||||
| import 'package:anyway/main.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:permission_handler/permission_handler.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
| import 'package:url_launcher/url_launcher.dart'; | ||||
|  | ||||
|  | ||||
| bool debugMode = false; | ||||
|  | ||||
| class SettingsPage extends StatefulWidget { | ||||
|   @override | ||||
|   _SettingsPageState createState() => _SettingsPageState(); | ||||
| } | ||||
|  | ||||
| class _SettingsPageState extends State<SettingsPage> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return ListView( | ||||
|       padding: EdgeInsets.all(15), | ||||
|       children: [ | ||||
|         // First a round, centered image | ||||
|         Center( | ||||
|           child: CircleAvatar( | ||||
|             radius: 75, | ||||
|             child: Icon(Icons.settings, size: 100), | ||||
|           ) | ||||
|         ), | ||||
|         Center( | ||||
|           child: Text('Global settings', style: TextStyle(fontSize: 24)) | ||||
|         ), | ||||
|  | ||||
|         Divider(indent: 25, endIndent: 25, height: 50), | ||||
|  | ||||
|         darkMode(), | ||||
|         setLocationUsage(), | ||||
|         setDebugMode(), | ||||
|  | ||||
|         Divider(indent: 25, endIndent: 25, height: 50), | ||||
|  | ||||
|         privacyInfo(), | ||||
|       ] | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget setDebugMode() { | ||||
|     return Row( | ||||
|       children: [ | ||||
|         Text('Debugging: use a custom API URL'), | ||||
|         // white space | ||||
|         Spacer(), | ||||
|         Switch( | ||||
|           value: debugMode, | ||||
|           onChanged: (bool? newValue) { | ||||
|             setState(() { | ||||
|               debugMode = newValue!; | ||||
|               if (debugMode) { | ||||
|                 showDialog( | ||||
|                   context: context, | ||||
|                   builder: (BuildContext context) { | ||||
|                     return AlertDialog( | ||||
|                       title: Text('Debug mode - use a custom API endpoint'), | ||||
|                       content: TextField( | ||||
|                         controller: TextEditingController(text: API_URL_DEBUG), | ||||
|                         onChanged: (value) { | ||||
|                           setState(() { | ||||
|                             API_URL_BASE = value; | ||||
|                           }); | ||||
|                         }, | ||||
|                       ), | ||||
|                       actions: [ | ||||
|                         TextButton( | ||||
|                           child: Text('OK'), | ||||
|                           onPressed: () { | ||||
|                             Navigator.of(context).pop(); | ||||
|                           }, | ||||
|                         ), | ||||
|                       ], | ||||
|                     ); | ||||
|                   } | ||||
|                 ); | ||||
|               } | ||||
|             }); | ||||
|           } | ||||
|         ) | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget darkMode() { | ||||
|     return Row( | ||||
|       children: [ | ||||
|         Text('Dark mode'), | ||||
|         Spacer(), | ||||
|         Switch( | ||||
|           value: Theme.of(context).brightness == Brightness.dark, | ||||
|           onChanged: (bool? newValue) { | ||||
|             setState(() { | ||||
|               rootScaffoldMessengerKey.currentState!.showSnackBar( | ||||
|                 SnackBar(content: Text('Dark mode is not implemented yet')) | ||||
|               ); | ||||
|               // if (newValue!) { | ||||
|               //   // Dark mode | ||||
|               //   Theme.of(context).brightness = Brightness.dark; | ||||
|               // } else { | ||||
|               //   // Light mode | ||||
|               //   Theme.of(context).brightness = Brightness.light; | ||||
|               // } | ||||
|             }); | ||||
|           } | ||||
|         ) | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget setLocationUsage() { | ||||
|     Future<SharedPreferences> preferences = SharedPreferences.getInstance(); | ||||
|     return Row( | ||||
|       children: [ | ||||
|         Text('Use location services'), | ||||
|         // white space | ||||
|         Spacer(), | ||||
|         FutureBuilder( | ||||
|           future: preferences, | ||||
|           builder: (context, snapshot) { | ||||
|             if (snapshot.hasData) { | ||||
|               bool useLocation = snapshot.data!.getBool('useLocation') ?? false; | ||||
|               return Switch( | ||||
|                 value: useLocation, | ||||
|                 onChanged: setUseLocation, | ||||
|               ); | ||||
|             } else { | ||||
|               return CircularProgressIndicator(); | ||||
|             } | ||||
|           } | ||||
|         ) | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void setUseLocation(bool newValue) async { | ||||
|     await Permission.locationWhenInUse | ||||
|       .onDeniedCallback(() { | ||||
|         rootScaffoldMessengerKey.currentState!.showSnackBar( | ||||
|           SnackBar(content: Text('Location services are required for this feature')) | ||||
|         ); | ||||
|       }) | ||||
|       .onGrantedCallback(() { | ||||
|         rootScaffoldMessengerKey.currentState!.showSnackBar( | ||||
|           SnackBar(content: Text('Location services are now enabled')) | ||||
|         ); | ||||
|         SharedPreferences.getInstance().then( | ||||
|           (SharedPreferences prefs) { | ||||
|             setState(() { | ||||
|               prefs.setBool('useLocation', newValue); | ||||
|             }); | ||||
|           } | ||||
|         ); | ||||
|       }) | ||||
|       .onPermanentlyDeniedCallback(() { | ||||
|         rootScaffoldMessengerKey.currentState!.showSnackBar( | ||||
|           SnackBar(content: Text('Location services are required for this feature')) | ||||
|         ); | ||||
|       })   | ||||
|     .request(); | ||||
|   } | ||||
|  | ||||
|   Widget privacyInfo() { | ||||
|     return Center( | ||||
|       child: Column( | ||||
|         children: [ | ||||
|           Text('Our privacy policy is available under:'), | ||||
|            | ||||
|           TextButton.icon( | ||||
|             icon: Icon(Icons.info), | ||||
|             label: Text(PRIVACY_URL), | ||||
|             onPressed: () async{ | ||||
|               await launchUrl(Uri.parse(PRIVACY_URL)); | ||||
|             } | ||||
|           ) | ||||
|         ], | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -4,13 +4,13 @@ import 'dart:convert'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
|  | ||||
| LandmarkType typeSightseeing = LandmarkType(name: 'sightseeing'); | ||||
| LandmarkType typeNature = LandmarkType(name: 'nature'); | ||||
| LandmarkType typeShopping = LandmarkType(name: 'shopping'); | ||||
| LandmarkType sightseeing = LandmarkType(name: 'sightseeing'); | ||||
| LandmarkType nature = LandmarkType(name: 'nature'); | ||||
| LandmarkType shopping = LandmarkType(name: 'shopping'); | ||||
| // LandmarkType museum = LandmarkType(name: 'Museum'); | ||||
| // LandmarkType restaurant = LandmarkType(name: 'Restaurant'); | ||||
| LandmarkType typeStart = LandmarkType(name: 'start'); | ||||
| LandmarkType typeFinish = LandmarkType(name: 'finish'); | ||||
| LandmarkType start = LandmarkType(name: 'start'); | ||||
| LandmarkType finish = LandmarkType(name: 'finish'); | ||||
|  | ||||
|  | ||||
| final class Landmark extends LinkedListEntry<Landmark>{ | ||||
| @@ -130,22 +130,22 @@ class LandmarkType { | ||||
|   LandmarkType({required this.name, this.icon = const Icon(Icons.location_on)}) { | ||||
|     switch (name) { | ||||
|       case 'sightseeing': | ||||
|         icon = const Icon(Icons.church); | ||||
|         icon = Icon(Icons.church); | ||||
|         break; | ||||
|       case 'nature': | ||||
|         icon = const Icon(Icons.eco); | ||||
|         icon = Icon(Icons.eco); | ||||
|         break; | ||||
|       case 'shopping': | ||||
|         icon = const Icon(Icons.shopping_cart); | ||||
|         icon = Icon(Icons.shopping_cart); | ||||
|         break; | ||||
|       case 'start': | ||||
|         icon = const Icon(Icons.play_arrow); | ||||
|         icon = Icon(Icons.play_arrow); | ||||
|         break; | ||||
|       case 'finish': | ||||
|         icon = const Icon(Icons.flag); | ||||
|         icon = Icon(Icons.flag); | ||||
|         break; | ||||
|       default: | ||||
|         icon = const Icon(Icons.location_on); | ||||
|         icon = Icon(Icons.location_on); | ||||
|     } | ||||
|   } | ||||
|   @override | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import 'package:anyway/structs/landmark.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
|  | ||||
|  | ||||
| class SinglePreference { | ||||
| @@ -20,6 +20,16 @@ class SinglePreference { | ||||
|     this.minVal = 0, | ||||
|     this.maxVal = 5, | ||||
|   }); | ||||
|  | ||||
|   void save() async { | ||||
|     SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); | ||||
|       sharedPrefs.setInt('pref_$slug', value); | ||||
|   } | ||||
|  | ||||
|   void load() async { | ||||
|     SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); | ||||
|       value = sharedPrefs.getInt('pref_$slug') ?? minVal; | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -29,41 +39,64 @@ class UserPreferences { | ||||
|     slug: "sightseeing", | ||||
|     description: "How much do you like sightseeing?", | ||||
|     value: 0, | ||||
|     icon: typeSightseeing.icon, | ||||
|     icon: Icon(Icons.church), | ||||
|   ); | ||||
|   SinglePreference shopping = SinglePreference( | ||||
|     name: "Shopping", | ||||
|     slug: "shopping", | ||||
|     description: "How much do you like shopping?", | ||||
|     value: 0, | ||||
|     icon: typeShopping.icon, | ||||
|     icon: Icon(Icons.shopping_bag), | ||||
|   ); | ||||
|   SinglePreference nature = SinglePreference( | ||||
|     name: "Nature", | ||||
|     slug: "nature", | ||||
|     description: "How much do you like nature?", | ||||
|     value: 0, | ||||
|     icon: typeNature.icon, | ||||
|     icon: Icon(Icons.landscape), | ||||
|   ); | ||||
|  | ||||
|   SinglePreference maxTime = SinglePreference( | ||||
|     name: "Trip duration", | ||||
|     slug: "duration", | ||||
|     description: "How long should your trip be?", | ||||
|     description: "How long do you want your trip to be?", | ||||
|     value: 30, | ||||
|     minVal: 30, | ||||
|     maxVal: 720, | ||||
|     icon: Icon(Icons.timer), | ||||
|   ); | ||||
|   SinglePreference maxDetour = SinglePreference( | ||||
|     name: "Trip detours", | ||||
|     slug: "detours", | ||||
|     description: "Are you okay with roaming even if makes the trip longer?", | ||||
|     value: 0, | ||||
|     maxVal: 30, | ||||
|     icon: Icon(Icons.loupe_sharp), | ||||
|   ); | ||||
|  | ||||
|  | ||||
|  | ||||
|   Future<void> load() async { | ||||
|     for (SinglePreference pref in [sightseeing, shopping, nature, maxTime, maxDetour]) { | ||||
|       pref.load(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Map<String, dynamic> toJson() { | ||||
|       // This is "opinionated" JSON, corresponding to the backend's expectations | ||||
|       return { | ||||
|         "sightseeing": {"type": "sightseeing", "score": sightseeing.value}, | ||||
|         "shopping": {"type": "shopping", "score": shopping.value}, | ||||
|         "nature": {"type": "nature", "score": nature.value}, | ||||
|         "max_time_minute": maxTime.value | ||||
|         "max_time_minute": maxTime.value, | ||||
|         "detour_tolerance_minute": maxDetour.value | ||||
|       }; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| Future<UserPreferences> loadUserPreferences() async { | ||||
|   UserPreferences prefs = UserPreferences(); | ||||
|   await prefs.load(); | ||||
|   return prefs; | ||||
| } | ||||
| @@ -29,41 +29,29 @@ Dio dio = Dio( | ||||
|  | ||||
| fetchTrip( | ||||
|   Trip trip, | ||||
|   UserPreferences preferences, | ||||
|   Future<UserPreferences> preferences, | ||||
| ) async { | ||||
|   UserPreferences prefs = await preferences; | ||||
|   Map<String, dynamic> data = { | ||||
|     "preferences": preferences.toJson(), | ||||
|     "preferences": prefs.toJson(), | ||||
|     "start": trip.landmarks!.first.location, | ||||
|   }; | ||||
|   String dataString = jsonEncode(data); | ||||
|   log(dataString); | ||||
|  | ||||
|   late Response response; | ||||
|   try { | ||||
|      response = await dio.post( | ||||
|       "/trip/new", | ||||
|       data: data | ||||
|     ); | ||||
|   } catch (e) { | ||||
|     trip.updateUUID("error"); | ||||
|     trip.updateError(e.toString()); | ||||
|     log(e.toString()); | ||||
|     return; | ||||
|   } | ||||
|   final response = await dio.post( | ||||
|     "/trip/new", | ||||
|     data: data | ||||
|   ); | ||||
|  | ||||
|   // handle errors | ||||
|   if (response.statusCode != 200) { | ||||
|     trip.updateUUID("error"); | ||||
|     String errorDetail; | ||||
|     if (response.data.runtimeType == String) { | ||||
|       errorDetail = response.data; | ||||
|     } else { | ||||
|       errorDetail = response.data["detail"] ?? "Unknown error"; | ||||
|     if (response.data["detail"] != null) { | ||||
|       trip.updateError(response.data["detail"]); | ||||
|       log(response.data["detail"]); | ||||
|       // throw Exception(response.data["detail"]); | ||||
|     } | ||||
|     trip.updateError(errorDetail); | ||||
|     log(errorDetail); | ||||
|     // Actualy no need to throw an exception, we can just log the error and let the user retry | ||||
|     // throw Exception(errorDetail); | ||||
|   } else { | ||||
|     Map<String, dynamic> json = response.data; | ||||
|  | ||||
|   | ||||
| @@ -5,14 +5,12 @@ | ||||
| import FlutterMacOS | ||||
| import Foundation | ||||
|  | ||||
| import geolocator_apple | ||||
| import path_provider_foundation | ||||
| import shared_preferences_foundation | ||||
| import sqflite | ||||
| import url_launcher_macos | ||||
|  | ||||
| func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { | ||||
|   GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) | ||||
|   PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) | ||||
|   SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) | ||||
|   SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) | ||||
|   | ||||
| @@ -45,26 +45,26 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: cached_network_image | ||||
|       sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" | ||||
|       sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.4.1" | ||||
|     version: "3.4.0" | ||||
|   cached_network_image_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: cached_network_image_platform_interface | ||||
|       sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" | ||||
|       sha256: ff0c949e323d2a1b52be73acce5b4a7b04063e61414c8ca542dbba47281630a7 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.1.1" | ||||
|     version: "4.1.0" | ||||
|   cached_network_image_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: cached_network_image_web | ||||
|       sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" | ||||
|       sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.3.1" | ||||
|     version: "1.3.0" | ||||
|   characters: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -109,10 +109,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: crypto | ||||
|       sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 | ||||
|       sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.0.5" | ||||
|     version: "3.0.3" | ||||
|   csslib: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -133,18 +133,18 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: dio | ||||
|       sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" | ||||
|       sha256: e17f6b3097b8c51b72c74c9f071a605c47bcc8893839bd66732457a5ebe73714 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "5.7.0" | ||||
|     version: "5.5.0+1" | ||||
|   dio_web_adapter: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: dio_web_adapter | ||||
|       sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" | ||||
|       sha256: "36c5b2d79eb17cdae41e974b7a8284fec631651d2a6f39a8a2ff22327e90aeac" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.0" | ||||
|     version: "1.0.1" | ||||
|   fake_async: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -157,10 +157,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: ffi | ||||
|       sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" | ||||
|       sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.1.3" | ||||
|     version: "2.1.2" | ||||
|   file: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -186,10 +186,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_cache_manager | ||||
|       sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" | ||||
|       sha256: a77f77806a790eb9ba0118a5a3a936e81c4fea2b61533033b2b0c3d50bbde5ea | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.4.1" | ||||
|     version: "3.4.0" | ||||
|   flutter_launcher_icons: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -210,10 +210,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_plugin_android_lifecycle | ||||
|       sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda" | ||||
|       sha256: "9d98bd47ef9d34e803d438f17fd32b116d31009f534a6fa5ce3a1167f189a6de" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.22" | ||||
|     version: "2.0.21" | ||||
|   flutter_svg: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -264,54 +264,6 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.2.0" | ||||
|   geolocator: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: geolocator | ||||
|       sha256: "0ec58b731776bc43097fcf751f79681b6a8f6d3bc737c94779fe9f1ad73c1a81" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "13.0.1" | ||||
|   geolocator_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: geolocator_android | ||||
|       sha256: "7aefc530db47d90d0580b552df3242440a10fe60814496a979aa67aa98b1fd47" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.6.1" | ||||
|   geolocator_apple: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: geolocator_apple | ||||
|       sha256: bc2aca02423ad429cb0556121f56e60360a2b7d694c8570301d06ea0c00732fd | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.7" | ||||
|   geolocator_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: geolocator_platform_interface | ||||
|       sha256: "386ce3d9cce47838355000070b1d0b13efb5bc430f8ecda7e9238c8409ace012" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.2.4" | ||||
|   geolocator_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: geolocator_web | ||||
|       sha256: "2ed69328e05cd94e7eb48bb0535f5fc0c0c44d1c4fa1e9737267484d05c29b5e" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.1.1" | ||||
|   geolocator_windows: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: geolocator_windows | ||||
|       sha256: "53da08937d07c24b0d9952eb57a3b474e29aae2abf9dd717f7e1230995f13f0e" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.2.3" | ||||
|   google_maps: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -324,42 +276,42 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: google_maps_flutter | ||||
|       sha256: "2e302fa3aaf4e2a297f0342d83ebc5e8e9f826e9a716aef473fe7f404ec630a7" | ||||
|       sha256: acf0ec482d86b2ac55ade80597ce7f797a47971f5210ebfd030f0d58130e0a94 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.9.0" | ||||
|     version: "2.7.0" | ||||
|   google_maps_flutter_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: google_maps_flutter_android | ||||
|       sha256: "10cf27bee8c560f8e69992b3a0f27ddf1d7acbea622ddb13ef3f587848a73f26" | ||||
|       sha256: "5d444f4135559488d7ea325eae710ae3284e6951b1b61729a0ac026456fe1548" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.14.7" | ||||
|     version: "2.12.1" | ||||
|   google_maps_flutter_ios: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: google_maps_flutter_ios | ||||
|       sha256: "3a484846fc56f15e47e3de1f5ea80a7ff2b31721d2faa88f390f3b3cf580c953" | ||||
|       sha256: a6e3c6ecdda6c985053f944be13a0645ebb919da2ef0f5bc579c5e1670a5b2a8 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.13.0" | ||||
|     version: "2.10.0" | ||||
|   google_maps_flutter_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: google_maps_flutter_platform_interface | ||||
|       sha256: "099874463dc4c9bff04fe4b2b8cf7284d2455c2deead8f9a59a87e1b9f028c69" | ||||
|       sha256: bd60ca330e3c7763b95b477054adec338a522d982af73ecc520b232474063ac5 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.9.2" | ||||
|     version: "2.8.0" | ||||
|   google_maps_flutter_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: google_maps_flutter_web | ||||
|       sha256: ff39211bd25d7fad125d19f757eba85bd154460907cd4d135e07e3d0f98a4130 | ||||
|       sha256: "8d5d0f58bfc4afac0bbe3d399f2018fcea691e3ea3d35254b7aae56df5827659" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.5.10" | ||||
|     version: "0.5.9+1" | ||||
|   html: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -436,10 +388,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: map_launcher | ||||
|       sha256: "7436d6ef9ae57ff15beafcedafe0a8f0604006cbecd2d26024c4cfb0158c2b9a" | ||||
|       sha256: af59b9f79f641022e06761c9d4217c6c57b9ef9020af2fdb23155ec87af79e61 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.5.0" | ||||
|     version: "3.3.1" | ||||
|   matcher: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -508,10 +460,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: path_provider_android | ||||
|       sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" | ||||
|       sha256: "490539678396d4c3c0b06efdaab75ae60675c3e0c66f72bc04c2e2c1e0e2abeb" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.2.10" | ||||
|     version: "2.2.9" | ||||
|   path_provider_foundation: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -544,54 +496,6 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.0" | ||||
|   permission_handler: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: permission_handler | ||||
|       sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "11.3.1" | ||||
|   permission_handler_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: permission_handler_android | ||||
|       sha256: "76e4ab092c1b240d31177bb64d2b0bea43f43d0e23541ec866151b9f7b2490fa" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "12.0.12" | ||||
|   permission_handler_apple: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: permission_handler_apple | ||||
|       sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "9.4.5" | ||||
|   permission_handler_html: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: permission_handler_html | ||||
|       sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.1.3+2" | ||||
|   permission_handler_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: permission_handler_platform_interface | ||||
|       sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.2.3" | ||||
|   permission_handler_windows: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: permission_handler_windows | ||||
|       sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.2.1" | ||||
|   petitparser: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -644,34 +548,34 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: shared_preferences | ||||
|       sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" | ||||
|       sha256: c3f888ba2d659f3e75f4686112cc1e71f46177f74452d40d8307edc332296ead | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.2" | ||||
|     version: "2.3.0" | ||||
|   shared_preferences_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_android | ||||
|       sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" | ||||
|       sha256: "041be4d9d2dc6079cf342bc8b761b03787e3b71192d658220a56cac9c04a0294" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.2" | ||||
|     version: "2.3.0" | ||||
|   shared_preferences_foundation: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_foundation | ||||
|       sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f | ||||
|       sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.5.2" | ||||
|     version: "2.5.0" | ||||
|   shared_preferences_linux: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_linux | ||||
|       sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" | ||||
|       sha256: "2ba0510d3017f91655b7543e9ee46d48619de2a2af38e5c790423f7007c7ccc1" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.1" | ||||
|     version: "2.4.0" | ||||
|   shared_preferences_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -684,18 +588,18 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_web | ||||
|       sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e | ||||
|       sha256: "3a293170d4d9403c3254ee05b84e62e8a9b3c5808ebd17de6a33fe9ea6457936" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.2" | ||||
|     version: "2.4.0" | ||||
|   shared_preferences_windows: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_windows | ||||
|       sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" | ||||
|       sha256: "398084b47b7f92110683cac45c6dc4aae853db47e470e5ddcd52cab7f7196ab2" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.1" | ||||
|     version: "2.4.0" | ||||
|   sky_engine: | ||||
|     dependency: transitive | ||||
|     description: flutter | ||||
| @@ -737,10 +641,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: sqflite_common | ||||
|       sha256: "4058172e418eb7e7f2058dcb7657d451a8fc264afa0dea4dbd0f304a57131611" | ||||
|       sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.5.4+3" | ||||
|     version: "2.5.4" | ||||
|   stack_trace: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -777,10 +681,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: synchronized | ||||
|       sha256: "51b08572b9f091f8c3eb4d9d4be253f196ff0075d5ec9b10a884026d5b55d7bc" | ||||
|       sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.3.0+2" | ||||
|     version: "3.1.0+1" | ||||
|   term_glyph: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -841,10 +745,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_macos | ||||
|       sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" | ||||
|       sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.2.1" | ||||
|     version: "3.2.0" | ||||
|   url_launcher_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -873,10 +777,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: uuid | ||||
|       sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 | ||||
|       sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.5.0" | ||||
|     version: "4.4.2" | ||||
|   vector_graphics: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -921,10 +825,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: web | ||||
|       sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb | ||||
|       sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.1.0" | ||||
|     version: "0.5.1" | ||||
|   widget_to_marker: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|   | ||||
| @@ -49,8 +49,6 @@ dependencies: | ||||
|   flutter_svg: ^2.0.10+1 | ||||
|   url_launcher: ^6.3.0 | ||||
|   flutter_launcher_icons: ^0.13.1 | ||||
|   permission_handler: ^11.3.1 | ||||
|   geolocator: ^13.0.1 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
| @@ -6,15 +6,9 @@ | ||||
|  | ||||
| #include "generated_plugin_registrant.h" | ||||
|  | ||||
| #include <geolocator_windows/geolocator_windows.h> | ||||
| #include <permission_handler_windows/permission_handler_windows_plugin.h> | ||||
| #include <url_launcher_windows/url_launcher_windows.h> | ||||
|  | ||||
| void RegisterPlugins(flutter::PluginRegistry* registry) { | ||||
|   GeolocatorWindowsRegisterWithRegistrar( | ||||
|       registry->GetRegistrarForPlugin("GeolocatorWindows")); | ||||
|   PermissionHandlerWindowsPluginRegisterWithRegistrar( | ||||
|       registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); | ||||
|   UrlLauncherWindowsRegisterWithRegistrar( | ||||
|       registry->GetRegistrarForPlugin("UrlLauncherWindows")); | ||||
| } | ||||
|   | ||||
| @@ -3,8 +3,6 @@ | ||||
| # | ||||
|  | ||||
| list(APPEND FLUTTER_PLUGIN_LIST | ||||
|   geolocator_windows | ||||
|   permission_handler_windows | ||||
|   url_launcher_windows | ||||
| ) | ||||
|  | ||||
|   | ||||