Compare commits
	
		
			16 Commits
		
	
	
		
			backend/fe
			...
			f6d0cd5360
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| f6d0cd5360 | |||
| 7a18830e99 | |||
| ba14a0279e | |||
| 5a2c61d343 | |||
| 5e27dd9d79 | |||
| d92001faaf | |||
| 73f0dc8361 | |||
| 05092e55f1 | |||
| 83be4b7616 | |||
| 8a9ec6b4d8 | |||
| 8c3145dfc9 | |||
| 2bf38119d6 | |||
| ca711c614f | |||
| 357edf3000 | |||
| 444c47e3a4 | |||
| da6ab207d9 | 
							
								
								
									
										3
									
								
								backend/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								backend/.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,6 +1,9 @@
 | 
				
			|||||||
# osm-cache
 | 
					# osm-cache
 | 
				
			||||||
cache_XML/
 | 
					cache_XML/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# secrets
 | 
				
			||||||
 | 
					*secrets.yaml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Byte-compiled / optimized / DLL files
 | 
					# Byte-compiled / optimized / DLL files
 | 
				
			||||||
__pycache__/
 | 
					__pycache__/
 | 
				
			||||||
*.py[cod]
 | 
					*.py[cod]
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										363
									
								
								backend/landmarks.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										363
									
								
								backend/landmarks.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,363 @@
 | 
				
			|||||||
 | 
					[
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        "name": "Chinatown",
 | 
				
			||||||
 | 
					        "type": "shopping",
 | 
				
			||||||
 | 
					        "location": [
 | 
				
			||||||
 | 
					            45.7554934,
 | 
				
			||||||
 | 
					            4.8444852
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "osm_type": "way",
 | 
				
			||||||
 | 
					        "osm_id": 996515596,
 | 
				
			||||||
 | 
					        "attractiveness": 129,
 | 
				
			||||||
 | 
					        "n_tags": 0,
 | 
				
			||||||
 | 
					        "image_url": null,
 | 
				
			||||||
 | 
					        "website_url": null,
 | 
				
			||||||
 | 
					        "wiki_url": null,
 | 
				
			||||||
 | 
					        "keywords": {},
 | 
				
			||||||
 | 
					        "description": null,
 | 
				
			||||||
 | 
					        "duration": 30,
 | 
				
			||||||
 | 
					        "name_en": null,
 | 
				
			||||||
 | 
					        "uuid": "285d159c-68ee-4b37-8d71-f27ee3d38b02",
 | 
				
			||||||
 | 
					        "must_do": false,
 | 
				
			||||||
 | 
					        "must_avoid": false,
 | 
				
			||||||
 | 
					        "is_secondary": false,
 | 
				
			||||||
 | 
					        "time_to_reach_next": 0,
 | 
				
			||||||
 | 
					        "next_uuid": null,
 | 
				
			||||||
 | 
					        "is_viewpoint": false,
 | 
				
			||||||
 | 
					        "is_place_of_worship": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        "name": "Galeries Lafayette",
 | 
				
			||||||
 | 
					        "type": "shopping",
 | 
				
			||||||
 | 
					        "location": [
 | 
				
			||||||
 | 
					            45.7627107,
 | 
				
			||||||
 | 
					            4.8556833
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "osm_type": "way",
 | 
				
			||||||
 | 
					        "osm_id": 1069872743,
 | 
				
			||||||
 | 
					        "attractiveness": 197,
 | 
				
			||||||
 | 
					        "n_tags": 11,
 | 
				
			||||||
 | 
					        "image_url": null,
 | 
				
			||||||
 | 
					        "website_url": "http://www.galerieslafayette.com/",
 | 
				
			||||||
 | 
					        "wiki_url": null,
 | 
				
			||||||
 | 
					        "keywords": null,
 | 
				
			||||||
 | 
					        "description": null,
 | 
				
			||||||
 | 
					        "duration": 30,
 | 
				
			||||||
 | 
					        "name_en": null,
 | 
				
			||||||
 | 
					        "uuid": "28f1bc30-10d3-4944-8861-0ed9abca012d",
 | 
				
			||||||
 | 
					        "must_do": false,
 | 
				
			||||||
 | 
					        "must_avoid": false,
 | 
				
			||||||
 | 
					        "is_secondary": false,
 | 
				
			||||||
 | 
					        "time_to_reach_next": 0,
 | 
				
			||||||
 | 
					        "next_uuid": null,
 | 
				
			||||||
 | 
					        "is_viewpoint": false,
 | 
				
			||||||
 | 
					        "is_place_of_worship": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        "name": "Muji",
 | 
				
			||||||
 | 
					        "type": "shopping",
 | 
				
			||||||
 | 
					        "location": [
 | 
				
			||||||
 | 
					            45.7615971,
 | 
				
			||||||
 | 
					            4.8543781
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "osm_type": "way",
 | 
				
			||||||
 | 
					        "osm_id": 1044165817,
 | 
				
			||||||
 | 
					        "attractiveness": 259,
 | 
				
			||||||
 | 
					        "n_tags": 14,
 | 
				
			||||||
 | 
					        "image_url": null,
 | 
				
			||||||
 | 
					        "website_url": "https://www.muji.com/fr/",
 | 
				
			||||||
 | 
					        "wiki_url": null,
 | 
				
			||||||
 | 
					        "keywords": null,
 | 
				
			||||||
 | 
					        "description": null,
 | 
				
			||||||
 | 
					        "duration": 30,
 | 
				
			||||||
 | 
					        "name_en": "Muji",
 | 
				
			||||||
 | 
					        "uuid": "957f86a5-6c00-41a2-815d-d6f739052be4",
 | 
				
			||||||
 | 
					        "must_do": false,
 | 
				
			||||||
 | 
					        "must_avoid": false,
 | 
				
			||||||
 | 
					        "is_secondary": false,
 | 
				
			||||||
 | 
					        "time_to_reach_next": 0,
 | 
				
			||||||
 | 
					        "next_uuid": null,
 | 
				
			||||||
 | 
					        "is_viewpoint": false,
 | 
				
			||||||
 | 
					        "is_place_of_worship": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        "name": "HEMA",
 | 
				
			||||||
 | 
					        "type": "shopping",
 | 
				
			||||||
 | 
					        "location": [
 | 
				
			||||||
 | 
					            45.7619133,
 | 
				
			||||||
 | 
					            4.8565239
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "osm_type": "way",
 | 
				
			||||||
 | 
					        "osm_id": 1069872750,
 | 
				
			||||||
 | 
					        "attractiveness": 156,
 | 
				
			||||||
 | 
					        "n_tags": 9,
 | 
				
			||||||
 | 
					        "image_url": null,
 | 
				
			||||||
 | 
					        "website_url": "https://fr.westfield.com/lapartdieu/store/HEMA/www.hema.fr",
 | 
				
			||||||
 | 
					        "wiki_url": null,
 | 
				
			||||||
 | 
					        "keywords": null,
 | 
				
			||||||
 | 
					        "description": null,
 | 
				
			||||||
 | 
					        "duration": 30,
 | 
				
			||||||
 | 
					        "name_en": null,
 | 
				
			||||||
 | 
					        "uuid": "8dae9d3e-e4c4-4e80-941d-0b106e22c85b",
 | 
				
			||||||
 | 
					        "must_do": false,
 | 
				
			||||||
 | 
					        "must_avoid": false,
 | 
				
			||||||
 | 
					        "is_secondary": false,
 | 
				
			||||||
 | 
					        "time_to_reach_next": 0,
 | 
				
			||||||
 | 
					        "next_uuid": null,
 | 
				
			||||||
 | 
					        "is_viewpoint": false,
 | 
				
			||||||
 | 
					        "is_place_of_worship": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        "name": "Cordeliers",
 | 
				
			||||||
 | 
					        "type": "shopping",
 | 
				
			||||||
 | 
					        "location": [
 | 
				
			||||||
 | 
					            45.7622752,
 | 
				
			||||||
 | 
					            4.8337998
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "osm_type": "node",
 | 
				
			||||||
 | 
					        "osm_id": 5545183519,
 | 
				
			||||||
 | 
					        "attractiveness": 813,
 | 
				
			||||||
 | 
					        "n_tags": 0,
 | 
				
			||||||
 | 
					        "image_url": null,
 | 
				
			||||||
 | 
					        "website_url": null,
 | 
				
			||||||
 | 
					        "wiki_url": null,
 | 
				
			||||||
 | 
					        "keywords": {},
 | 
				
			||||||
 | 
					        "description": null,
 | 
				
			||||||
 | 
					        "duration": 30,
 | 
				
			||||||
 | 
					        "name_en": null,
 | 
				
			||||||
 | 
					        "uuid": "ba02adb5-e28f-4645-8c2d-25ead6232379",
 | 
				
			||||||
 | 
					        "must_do": false,
 | 
				
			||||||
 | 
					        "must_avoid": false,
 | 
				
			||||||
 | 
					        "is_secondary": false,
 | 
				
			||||||
 | 
					        "time_to_reach_next": 0,
 | 
				
			||||||
 | 
					        "next_uuid": null,
 | 
				
			||||||
 | 
					        "is_viewpoint": false,
 | 
				
			||||||
 | 
					        "is_place_of_worship": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        "name": "Halles de Lyon Paul Bocuse",
 | 
				
			||||||
 | 
					        "type": "shopping",
 | 
				
			||||||
 | 
					        "location": [
 | 
				
			||||||
 | 
					            45.7628282,
 | 
				
			||||||
 | 
					            4.8505601
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "osm_type": "relation",
 | 
				
			||||||
 | 
					        "osm_id": 971529,
 | 
				
			||||||
 | 
					        "attractiveness": 272,
 | 
				
			||||||
 | 
					        "n_tags": 12,
 | 
				
			||||||
 | 
					        "image_url": null,
 | 
				
			||||||
 | 
					        "website_url": "https://www.halles-de-lyon-paulbocuse.com/",
 | 
				
			||||||
 | 
					        "wiki_url": "fr:Halles de Lyon-Paul Bocuse",
 | 
				
			||||||
 | 
					        "keywords": {
 | 
				
			||||||
 | 
					            "importance": "national",
 | 
				
			||||||
 | 
					            "height": null,
 | 
				
			||||||
 | 
					            "place_type": "marketplace",
 | 
				
			||||||
 | 
					            "date": null
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "description": "Halles de Lyon Paul Bocuse is a marketplace of national importance.",
 | 
				
			||||||
 | 
					        "duration": 30,
 | 
				
			||||||
 | 
					        "name_en": null,
 | 
				
			||||||
 | 
					        "uuid": "bbd50de3-aa91-425d-90c2-d4abfd1b4abe",
 | 
				
			||||||
 | 
					        "must_do": false,
 | 
				
			||||||
 | 
					        "must_avoid": false,
 | 
				
			||||||
 | 
					        "is_secondary": false,
 | 
				
			||||||
 | 
					        "time_to_reach_next": 0,
 | 
				
			||||||
 | 
					        "next_uuid": null,
 | 
				
			||||||
 | 
					        "is_viewpoint": false,
 | 
				
			||||||
 | 
					        "is_place_of_worship": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        "name": "Grand Bazar",
 | 
				
			||||||
 | 
					        "type": "shopping",
 | 
				
			||||||
 | 
					        "location": [
 | 
				
			||||||
 | 
					            45.7632141,
 | 
				
			||||||
 | 
					            4.8361975
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "osm_type": "way",
 | 
				
			||||||
 | 
					        "osm_id": 82399951,
 | 
				
			||||||
 | 
					        "attractiveness": 93,
 | 
				
			||||||
 | 
					        "n_tags": 7,
 | 
				
			||||||
 | 
					        "image_url": null,
 | 
				
			||||||
 | 
					        "website_url": null,
 | 
				
			||||||
 | 
					        "wiki_url": null,
 | 
				
			||||||
 | 
					        "keywords": null,
 | 
				
			||||||
 | 
					        "description": null,
 | 
				
			||||||
 | 
					        "duration": 30,
 | 
				
			||||||
 | 
					        "name_en": null,
 | 
				
			||||||
 | 
					        "uuid": "3de9131c-87c5-4efb-9fa8-064896fb8b29",
 | 
				
			||||||
 | 
					        "must_do": false,
 | 
				
			||||||
 | 
					        "must_avoid": false,
 | 
				
			||||||
 | 
					        "is_secondary": false,
 | 
				
			||||||
 | 
					        "time_to_reach_next": 0,
 | 
				
			||||||
 | 
					        "next_uuid": null,
 | 
				
			||||||
 | 
					        "is_viewpoint": false,
 | 
				
			||||||
 | 
					        "is_place_of_worship": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        "name": "Shopping Area",
 | 
				
			||||||
 | 
					        "type": "shopping",
 | 
				
			||||||
 | 
					        "location": [
 | 
				
			||||||
 | 
					            45.7673452,
 | 
				
			||||||
 | 
					            4.8438683
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "osm_type": "node",
 | 
				
			||||||
 | 
					        "osm_id": 0,
 | 
				
			||||||
 | 
					        "attractiveness": 156,
 | 
				
			||||||
 | 
					        "n_tags": 0,
 | 
				
			||||||
 | 
					        "image_url": null,
 | 
				
			||||||
 | 
					        "website_url": null,
 | 
				
			||||||
 | 
					        "wiki_url": null,
 | 
				
			||||||
 | 
					        "keywords": {},
 | 
				
			||||||
 | 
					        "description": null,
 | 
				
			||||||
 | 
					        "duration": 30,
 | 
				
			||||||
 | 
					        "name_en": null,
 | 
				
			||||||
 | 
					        "uuid": "df2482a8-7e2e-4536-aad3-564899b2fa65",
 | 
				
			||||||
 | 
					        "must_do": false,
 | 
				
			||||||
 | 
					        "must_avoid": false,
 | 
				
			||||||
 | 
					        "is_secondary": false,
 | 
				
			||||||
 | 
					        "time_to_reach_next": 0,
 | 
				
			||||||
 | 
					        "next_uuid": null,
 | 
				
			||||||
 | 
					        "is_viewpoint": false,
 | 
				
			||||||
 | 
					        "is_place_of_worship": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        "name": "Cour Oxyg\u00e8ne",
 | 
				
			||||||
 | 
					        "type": "shopping",
 | 
				
			||||||
 | 
					        "location": [
 | 
				
			||||||
 | 
					            45.7620905,
 | 
				
			||||||
 | 
					            4.8568873
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "osm_type": "way",
 | 
				
			||||||
 | 
					        "osm_id": 132673030,
 | 
				
			||||||
 | 
					        "attractiveness": 63,
 | 
				
			||||||
 | 
					        "n_tags": 5,
 | 
				
			||||||
 | 
					        "image_url": null,
 | 
				
			||||||
 | 
					        "website_url": null,
 | 
				
			||||||
 | 
					        "wiki_url": null,
 | 
				
			||||||
 | 
					        "keywords": null,
 | 
				
			||||||
 | 
					        "description": null,
 | 
				
			||||||
 | 
					        "duration": 30,
 | 
				
			||||||
 | 
					        "name_en": null,
 | 
				
			||||||
 | 
					        "uuid": "ed134f76-9a02-4bee-9c10-78454f7bc4ce",
 | 
				
			||||||
 | 
					        "must_do": false,
 | 
				
			||||||
 | 
					        "must_avoid": false,
 | 
				
			||||||
 | 
					        "is_secondary": false,
 | 
				
			||||||
 | 
					        "time_to_reach_next": 0,
 | 
				
			||||||
 | 
					        "next_uuid": null,
 | 
				
			||||||
 | 
					        "is_viewpoint": false,
 | 
				
			||||||
 | 
					        "is_place_of_worship": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        "name": "P\u00f4le de Commerces et de Loisirs Confluence",
 | 
				
			||||||
 | 
					        "type": "shopping",
 | 
				
			||||||
 | 
					        "location": [
 | 
				
			||||||
 | 
					            45.7410414,
 | 
				
			||||||
 | 
					            4.8171031
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "osm_type": "way",
 | 
				
			||||||
 | 
					        "osm_id": 440270633,
 | 
				
			||||||
 | 
					        "attractiveness": 259,
 | 
				
			||||||
 | 
					        "n_tags": 14,
 | 
				
			||||||
 | 
					        "image_url": null,
 | 
				
			||||||
 | 
					        "website_url": "https://www.confluence.fr/",
 | 
				
			||||||
 | 
					        "wiki_url": null,
 | 
				
			||||||
 | 
					        "keywords": null,
 | 
				
			||||||
 | 
					        "description": null,
 | 
				
			||||||
 | 
					        "duration": 30,
 | 
				
			||||||
 | 
					        "name_en": null,
 | 
				
			||||||
 | 
					        "uuid": "dd7e2f5f-0e60-4560-b903-e5ded4b6e36a",
 | 
				
			||||||
 | 
					        "must_do": false,
 | 
				
			||||||
 | 
					        "must_avoid": false,
 | 
				
			||||||
 | 
					        "is_secondary": false,
 | 
				
			||||||
 | 
					        "time_to_reach_next": 0,
 | 
				
			||||||
 | 
					        "next_uuid": null,
 | 
				
			||||||
 | 
					        "is_viewpoint": false,
 | 
				
			||||||
 | 
					        "is_place_of_worship": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        "name": "Grand H\u00f4tel-Dieu",
 | 
				
			||||||
 | 
					        "type": "shopping",
 | 
				
			||||||
 | 
					        "location": [
 | 
				
			||||||
 | 
					            45.7586955,
 | 
				
			||||||
 | 
					            4.8364597
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "osm_type": "relation",
 | 
				
			||||||
 | 
					        "osm_id": 300128,
 | 
				
			||||||
 | 
					        "attractiveness": 546,
 | 
				
			||||||
 | 
					        "n_tags": 22,
 | 
				
			||||||
 | 
					        "image_url": null,
 | 
				
			||||||
 | 
					        "website_url": "https://grand-hotel-dieu.com",
 | 
				
			||||||
 | 
					        "wiki_url": "fr:H\u00f4tel-Dieu de Lyon",
 | 
				
			||||||
 | 
					        "keywords": {
 | 
				
			||||||
 | 
					            "importance": "international",
 | 
				
			||||||
 | 
					            "height": null,
 | 
				
			||||||
 | 
					            "place_type": "building",
 | 
				
			||||||
 | 
					            "date": "C17"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "description": "Grand H\u00f4tel-Dieu is an internationally famous building. It was constructed in C17.",
 | 
				
			||||||
 | 
					        "duration": 30,
 | 
				
			||||||
 | 
					        "name_en": null,
 | 
				
			||||||
 | 
					        "uuid": "a91265a8-ffbd-44f7-a7ab-3ff75f08fbab",
 | 
				
			||||||
 | 
					        "must_do": false,
 | 
				
			||||||
 | 
					        "must_avoid": false,
 | 
				
			||||||
 | 
					        "is_secondary": false,
 | 
				
			||||||
 | 
					        "time_to_reach_next": 0,
 | 
				
			||||||
 | 
					        "next_uuid": null,
 | 
				
			||||||
 | 
					        "is_viewpoint": false,
 | 
				
			||||||
 | 
					        "is_place_of_worship": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        "name": "Westfield La Part-Dieu",
 | 
				
			||||||
 | 
					        "type": "shopping",
 | 
				
			||||||
 | 
					        "location": [
 | 
				
			||||||
 | 
					            45.761331,
 | 
				
			||||||
 | 
					            4.855676
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "osm_type": "way",
 | 
				
			||||||
 | 
					        "osm_id": 62338376,
 | 
				
			||||||
 | 
					        "attractiveness": 546,
 | 
				
			||||||
 | 
					        "n_tags": 22,
 | 
				
			||||||
 | 
					        "image_url": null,
 | 
				
			||||||
 | 
					        "website_url": "https://fr.westfield.com/lapartdieu",
 | 
				
			||||||
 | 
					        "wiki_url": "fr:La Part-Dieu (centre commercial)",
 | 
				
			||||||
 | 
					        "keywords": null,
 | 
				
			||||||
 | 
					        "description": null,
 | 
				
			||||||
 | 
					        "duration": 30,
 | 
				
			||||||
 | 
					        "name_en": null,
 | 
				
			||||||
 | 
					        "uuid": "7d60316f-d689-4fcf-be68-ffc09353b826",
 | 
				
			||||||
 | 
					        "must_do": false,
 | 
				
			||||||
 | 
					        "must_avoid": false,
 | 
				
			||||||
 | 
					        "is_secondary": false,
 | 
				
			||||||
 | 
					        "time_to_reach_next": 0,
 | 
				
			||||||
 | 
					        "next_uuid": null,
 | 
				
			||||||
 | 
					        "is_viewpoint": false,
 | 
				
			||||||
 | 
					        "is_place_of_worship": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        "name": "Ainay",
 | 
				
			||||||
 | 
					        "type": "shopping",
 | 
				
			||||||
 | 
					        "location": [
 | 
				
			||||||
 | 
					            45.7553105,
 | 
				
			||||||
 | 
					            4.8312084
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "osm_type": "node",
 | 
				
			||||||
 | 
					        "osm_id": 5545126047,
 | 
				
			||||||
 | 
					        "attractiveness": 132,
 | 
				
			||||||
 | 
					        "n_tags": 0,
 | 
				
			||||||
 | 
					        "image_url": null,
 | 
				
			||||||
 | 
					        "website_url": null,
 | 
				
			||||||
 | 
					        "wiki_url": null,
 | 
				
			||||||
 | 
					        "keywords": {},
 | 
				
			||||||
 | 
					        "description": null,
 | 
				
			||||||
 | 
					        "duration": 30,
 | 
				
			||||||
 | 
					        "name_en": null,
 | 
				
			||||||
 | 
					        "uuid": "ad214f3d-a4b9-4078-876a-446caa7ab01c",
 | 
				
			||||||
 | 
					        "must_do": false,
 | 
				
			||||||
 | 
					        "must_avoid": false,
 | 
				
			||||||
 | 
					        "is_secondary": false,
 | 
				
			||||||
 | 
					        "time_to_reach_next": 0,
 | 
				
			||||||
 | 
					        "next_uuid": null,
 | 
				
			||||||
 | 
					        "is_viewpoint": false,
 | 
				
			||||||
 | 
					        "is_place_of_worship": false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										0
									
								
								backend/src/landmarks/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								backend/src/landmarks/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -8,8 +8,8 @@ from pydantic import BaseModel
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from ..overpass.overpass import Overpass, get_base_info
 | 
					from ..overpass.overpass import Overpass, get_base_info
 | 
				
			||||||
from ..structs.landmark import Landmark
 | 
					from ..structs.landmark import Landmark
 | 
				
			||||||
from .get_time_distance import get_distance
 | 
					from ..utils.get_time_distance import get_distance
 | 
				
			||||||
from .utils import create_bbox
 | 
					from ..utils.bbox import create_bbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -103,7 +103,7 @@ class ClusterManager:
 | 
				
			|||||||
            out = out
 | 
					            out = out
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        except Exception as e:
 | 
					        except Exception as e:
 | 
				
			||||||
            self.logger.error(f"Error fetching clusters: {e}")
 | 
					            self.logger.warning(f"Error fetching clusters: {e}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if result is None :
 | 
					        if result is None :
 | 
				
			||||||
            self.logger.debug(f"Found no {cluster_type} clusters, overpass query returned no datapoints.")
 | 
					            self.logger.debug(f"Found no {cluster_type} clusters, overpass query returned no datapoints.")
 | 
				
			||||||
@@ -242,18 +242,16 @@ class ClusterManager:
 | 
				
			|||||||
                                                  out = 'ids center tags'
 | 
					                                                  out = 'ids center tags'
 | 
				
			||||||
                                                  )
 | 
					                                                  )
 | 
				
			||||||
            except Exception as e:
 | 
					            except Exception as e:
 | 
				
			||||||
                self.logger.error(f"Error fetching clusters: {e}")
 | 
					                self.logger.warning(f"Error fetching clusters: {e}")
 | 
				
			||||||
                continue
 | 
					                continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if result is None :
 | 
					            if result is None :
 | 
				
			||||||
                self.logger.error(f"Error fetching clusters: {e}")
 | 
					                self.logger.warning(f"Error fetching clusters: query result is None")
 | 
				
			||||||
                continue
 | 
					                continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            for elem in result:
 | 
					            for elem in result:
 | 
				
			||||||
                osm_type = elem.get('type')
 | 
					                # Get basic info
 | 
				
			||||||
 | 
					                id, coords, name = get_base_info(elem, elem.get('type'), with_name=True)
 | 
				
			||||||
                id, coords, name = get_base_info(elem, osm_type, with_name=True)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if name is None or coords is None :
 | 
					                if name is None or coords is None :
 | 
				
			||||||
                    continue
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -261,7 +259,7 @@ class ClusterManager:
 | 
				
			|||||||
                if  d < min_dist :
 | 
					                if  d < min_dist :
 | 
				
			||||||
                    min_dist = d
 | 
					                    min_dist = d
 | 
				
			||||||
                    new_name = name             # add name
 | 
					                    new_name = name             # add name
 | 
				
			||||||
                    osm_type = osm_type     # add type: 'way' or 'relation'
 | 
					                    osm_type = elem.get('type') # add type: 'way' or 'relation'
 | 
				
			||||||
                    osm_id = id                 # add OSM id
 | 
					                    osm_id = id                 # add OSM id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Landmark(
 | 
					        return Landmark(
 | 
				
			||||||
@@ -4,10 +4,10 @@ import yaml
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from ..structs.preferences import Preferences
 | 
					from ..structs.preferences import Preferences
 | 
				
			||||||
from ..structs.landmark import Landmark
 | 
					from ..structs.landmark import Landmark
 | 
				
			||||||
from .take_most_important import take_most_important
 | 
					from ..utils.take_most_important import take_most_important
 | 
				
			||||||
from .cluster_manager import ClusterManager
 | 
					from .cluster_manager import ClusterManager
 | 
				
			||||||
from ..overpass.overpass import Overpass, get_base_info
 | 
					from ..overpass.overpass import Overpass, get_base_info
 | 
				
			||||||
from .utils import create_bbox
 | 
					from ..utils.bbox import create_bbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ..constants import AMENITY_SELECTORS_PATH, LANDMARK_PARAMETERS_PATH, OPTIMIZER_PARAMETERS_PATH
 | 
					from ..constants import AMENITY_SELECTORS_PATH, LANDMARK_PARAMETERS_PATH, OPTIMIZER_PARAMETERS_PATH
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -197,7 +197,7 @@ class LandmarkManager:
 | 
				
			|||||||
                    out = 'ids center tags'
 | 
					                    out = 'ids center tags'
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
            except Exception as e:
 | 
					            except Exception as e:
 | 
				
			||||||
                self.logger.error(f"Error fetching landmarks: {e}")
 | 
					                self.logger.debug(f"Failed to fetch landmarks, proceeding without: {str(e)}")
 | 
				
			||||||
                continue
 | 
					                continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return_list += self._to_landmarks(result, landmarktype, preference_level)
 | 
					            return_list += self._to_landmarks(result, landmarktype, preference_level)
 | 
				
			||||||
@@ -246,8 +246,6 @@ class LandmarkManager:
 | 
				
			|||||||
                                attractiveness=0,
 | 
					                                attractiveness=0,
 | 
				
			||||||
                                n_tags=len(tags))
 | 
					                                n_tags=len(tags))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # self.logger.debug('added landmark.')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # Browse through tags to add information to landmark.
 | 
					            # Browse through tags to add information to landmark.
 | 
				
			||||||
            for key, value in tags.items():
 | 
					            for key, value in tags.items():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -277,6 +275,7 @@ class LandmarkManager:
 | 
				
			|||||||
                if 'building:' in key or 'pay' in key :
 | 
					                if 'building:' in key or 'pay' in key :
 | 
				
			||||||
                    landmark.n_tags -= 1
 | 
					                    landmark.n_tags -= 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                # Set the duration.
 | 
					                # Set the duration.
 | 
				
			||||||
                if value in ['museum', 'aquarium', 'planetarium'] :
 | 
					                if value in ['museum', 'aquarium', 'planetarium'] :
 | 
				
			||||||
                    landmark.duration = 60
 | 
					                    landmark.duration = 60
 | 
				
			||||||
@@ -287,14 +286,138 @@ class LandmarkManager:
 | 
				
			|||||||
                    landmark.is_place_of_worship = False
 | 
					                    landmark.is_place_of_worship = False
 | 
				
			||||||
                    landmark.duration = 10
 | 
					                    landmark.duration = 10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            else:
 | 
					            landmark.description, landmark.keywords = self.description_and_keywords(tags)
 | 
				
			||||||
            self.set_landmark_score(landmark, landmarktype, preference_level)
 | 
					            self.set_landmark_score(landmark, landmarktype, preference_level)
 | 
				
			||||||
            landmarks.append(landmark)
 | 
					            landmarks.append(landmark)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            continue
 | 
					            continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return landmarks
 | 
					        return landmarks
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def description_and_keywords(self, tags: dict):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Generates a description and a set of keywords for a given landmark based on its tags.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Params:
 | 
				
			||||||
 | 
					            tags (dict): A dictionary containing metadata about the landmark, including its name,
 | 
				
			||||||
 | 
					                    importance, height, date of construction, and visitor information.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            description (str): A string description of the landmark.
 | 
				
			||||||
 | 
					            keywords (dict): A dictionary of keywords with fields such as 'importance', 'height',
 | 
				
			||||||
 | 
					                        'place_type', and 'date'.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        # Extract relevant fields
 | 
				
			||||||
 | 
					        name = tags.get('name')
 | 
				
			||||||
 | 
					        importance = tags.get('importance', None)
 | 
				
			||||||
 | 
					        n_visitors = tags.get('tourism:visitors', None)
 | 
				
			||||||
 | 
					        height = tags.get('height')
 | 
				
			||||||
 | 
					        place_type = self.get_place_type(tags)
 | 
				
			||||||
 | 
					        date = self.get_date(tags)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if place_type is None :
 | 
				
			||||||
 | 
					            return None, None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Start the description.
 | 
				
			||||||
 | 
					        if importance is None :
 | 
				
			||||||
 | 
					            if len(tags.keys()) < 5 :
 | 
				
			||||||
 | 
					                return None, None
 | 
				
			||||||
 | 
					            if len(tags.keys()) < 10 :
 | 
				
			||||||
 | 
					                description = f"{name} is a well known {place_type}."
 | 
				
			||||||
 | 
					            elif len(tags.keys()) < 17 :
 | 
				
			||||||
 | 
					                importance = 'national'
 | 
				
			||||||
 | 
					                description = f"{name} is a {place_type} of national importance."
 | 
				
			||||||
 | 
					            else :
 | 
				
			||||||
 | 
					                importance = 'international'
 | 
				
			||||||
 | 
					                description = f"{name} is an internationally famous {place_type}."
 | 
				
			||||||
 | 
					        else :
 | 
				
			||||||
 | 
					            description = f"{name} is a {place_type} of {importance} importance."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if height is not None and date is not None :
 | 
				
			||||||
 | 
					            description += f" This {place_type} was constructed in {date} and is ca. {height} meters high."
 | 
				
			||||||
 | 
					        elif height is not None :
 | 
				
			||||||
 | 
					            description += f" This {place_type} stands ca. {height} meters tall."
 | 
				
			||||||
 | 
					        elif date is not None:
 | 
				
			||||||
 | 
					            description += f" It was constructed in {date}."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Format the visitor number
 | 
				
			||||||
 | 
					        if n_visitors is not None :
 | 
				
			||||||
 | 
					            n_visitors = int(n_visitors)
 | 
				
			||||||
 | 
					            if n_visitors < 1000000 :
 | 
				
			||||||
 | 
					                description += f" It welcomes {int(n_visitors/1000)} thousand visitors every year."
 | 
				
			||||||
 | 
					            else :
 | 
				
			||||||
 | 
					                description += f" It welcomes {round(n_visitors/1000000, 1)} million visitors every year."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Set the keywords.
 | 
				
			||||||
 | 
					        keywords = {"importance": importance,
 | 
				
			||||||
 | 
					                    "height": height,
 | 
				
			||||||
 | 
					                    "place_type": place_type,
 | 
				
			||||||
 | 
					                    "date": date}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return description, keywords
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_place_type(self, data):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Determines the type of the place based on available tags such as 'amenity', 'building',
 | 
				
			||||||
 | 
					        'historic', and 'leisure'. The priority order is: 'historic' > 'building' (if not generic) > 
 | 
				
			||||||
 | 
					        'amenity' > 'leisure'.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Params:
 | 
				
			||||||
 | 
					            data (dict): A dictionary containing metadata about the place.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            place_type (str): The determined type of the place, or None if no relevant type is found.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        amenity = data.get('amenity', None)
 | 
				
			||||||
 | 
					        building = data.get('building', None)
 | 
				
			||||||
 | 
					        historic = data.get('historic', None)
 | 
				
			||||||
 | 
					        leisure = data.get('leisure')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if historic and historic != "yes":
 | 
				
			||||||
 | 
					            return historic
 | 
				
			||||||
 | 
					        if building and building not in ["yes", "civic", "government", "apartments", "residential", "commericial", "industrial", "retail", "religious", "public", "service"]:
 | 
				
			||||||
 | 
					            return building
 | 
				
			||||||
 | 
					        if amenity:
 | 
				
			||||||
 | 
					            return amenity
 | 
				
			||||||
 | 
					        if leisure:
 | 
				
			||||||
 | 
					            return leisure
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_date(self, data):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Extracts the most relevant date from the available tags, prioritizing 'construction_date',
 | 
				
			||||||
 | 
					        'start_date', 'year_of_construction', and 'opening_date' in that order.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Params:
 | 
				
			||||||
 | 
					            data (dict): A dictionary containing metadata about the place.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            date (str): The most relevant date found, or None if no date is available.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        construction_date = data.get('construction_date', None)
 | 
				
			||||||
 | 
					        opening_date = data.get('opening_date', None)
 | 
				
			||||||
 | 
					        start_date = data.get('start_date', None)
 | 
				
			||||||
 | 
					        year_of_construction = data.get('year_of_construction', None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Prioritize based on availability
 | 
				
			||||||
 | 
					        if construction_date:
 | 
				
			||||||
 | 
					            return construction_date
 | 
				
			||||||
 | 
					        if start_date:
 | 
				
			||||||
 | 
					            return start_date
 | 
				
			||||||
 | 
					        if year_of_construction:
 | 
				
			||||||
 | 
					            return year_of_construction
 | 
				
			||||||
 | 
					        if opening_date:
 | 
				
			||||||
 | 
					            return opening_date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def dict_to_selector_list(d: dict) -> list:
 | 
					def dict_to_selector_list(d: dict) -> list:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Convert a dictionary of key-value pairs to a list of Overpass query strings.
 | 
					    Convert a dictionary of key-value pairs to a list of Overpass query strings.
 | 
				
			||||||
@@ -1,17 +1,16 @@
 | 
				
			|||||||
"""Main app for backend api"""
 | 
					"""Main app for backend api"""
 | 
				
			||||||
 | 
					 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
import time
 | 
					import time
 | 
				
			||||||
from contextlib import asynccontextmanager
 | 
					from contextlib import asynccontextmanager
 | 
				
			||||||
from fastapi import FastAPI, HTTPException, BackgroundTasks, Query
 | 
					from fastapi import FastAPI, HTTPException, BackgroundTasks
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .logging_config import configure_logging
 | 
					from .logging_config import configure_logging
 | 
				
			||||||
from .structs.landmark import Landmark, Toilets
 | 
					from .structs.landmark import Landmark
 | 
				
			||||||
from .structs.preferences import Preferences
 | 
					from .structs.preferences import Preferences
 | 
				
			||||||
from .structs.linked_landmarks import LinkedLandmarks
 | 
					from .structs.linked_landmarks import LinkedLandmarks
 | 
				
			||||||
from .structs.trip import Trip
 | 
					from .structs.trip import Trip
 | 
				
			||||||
from .utils.landmarks_manager import LandmarkManager
 | 
					from .landmarks.landmarks_manager import LandmarkManager
 | 
				
			||||||
from .utils.toilets_manager import ToiletsManager
 | 
					from .toilets.toilet_routes import router as toilets_router
 | 
				
			||||||
from .optimization.optimizer import Optimizer
 | 
					from .optimization.optimizer import Optimizer
 | 
				
			||||||
from .optimization.refiner import Refiner
 | 
					from .optimization.refiner import Refiner
 | 
				
			||||||
from .overpass.overpass import fill_cache
 | 
					from .overpass.overpass import fill_cache
 | 
				
			||||||
@@ -37,6 +36,10 @@ async def lifespan(app: FastAPI):
 | 
				
			|||||||
app = FastAPI(lifespan=lifespan)
 | 
					app = FastAPI(lifespan=lifespan)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.include_router(toilets_router)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.post("/trip/new")
 | 
					@app.post("/trip/new")
 | 
				
			||||||
def new_trip(preferences: Preferences,
 | 
					def new_trip(preferences: Preferences,
 | 
				
			||||||
             start: tuple[float, float],
 | 
					             start: tuple[float, float],
 | 
				
			||||||
@@ -66,6 +69,8 @@ def new_trip(preferences: Preferences,
 | 
				
			|||||||
        end = start
 | 
					        end = start
 | 
				
			||||||
        logger.info("No end coordinates provided. Using start=end.")
 | 
					        logger.info("No end coordinates provided. Using start=end.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    logger.info(f"Requested new trip generation. Details:\n\tCoordinates: {start}\n\tTime: {preferences.max_time_minute}\n\tSightseeing: {preferences.sightseeing.score}\n\tNature: {preferences.nature.score}\n\tShopping: {preferences.shopping.score}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    start_landmark = Landmark(name='start',
 | 
					    start_landmark = Landmark(name='start',
 | 
				
			||||||
                              type='start',
 | 
					                              type='start',
 | 
				
			||||||
                              location=(start[0], start[1]),
 | 
					                              location=(start[0], start[1]),
 | 
				
			||||||
@@ -87,6 +92,7 @@ def new_trip(preferences: Preferences,
 | 
				
			|||||||
                            n_tags=0)
 | 
					                            n_tags=0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    start_time = time.time()
 | 
					    start_time = time.time()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Generate the landmarks from the start location
 | 
					    # Generate the landmarks from the start location
 | 
				
			||||||
    landmarks, landmarks_short = manager.generate_landmarks_list(
 | 
					    landmarks, landmarks_short = manager.generate_landmarks_list(
 | 
				
			||||||
        center_coordinates = start,
 | 
					        center_coordinates = start,
 | 
				
			||||||
@@ -108,6 +114,7 @@ def new_trip(preferences: Preferences,
 | 
				
			|||||||
    try:
 | 
					    try:
 | 
				
			||||||
        base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short)
 | 
					        base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short)
 | 
				
			||||||
    except Exception as exc:
 | 
					    except Exception as exc:
 | 
				
			||||||
 | 
					        logger.error(f"Trip generation failed: {str(exc)}")
 | 
				
			||||||
        raise HTTPException(status_code=500, detail=f"Optimization failed: {str(exc)}") from exc
 | 
					        raise HTTPException(status_code=500, detail=f"Optimization failed: {str(exc)}") from exc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    t_first_stage = time.time() - start_time
 | 
					    t_first_stage = time.time() - start_time
 | 
				
			||||||
@@ -119,11 +126,9 @@ def new_trip(preferences: Preferences,
 | 
				
			|||||||
        refined_tour = refiner.refine_optimization(landmarks, base_tour,
 | 
					        refined_tour = refiner.refine_optimization(landmarks, base_tour,
 | 
				
			||||||
                                               preferences.max_time_minute,
 | 
					                                               preferences.max_time_minute,
 | 
				
			||||||
                                               preferences.detour_tolerance_minute)
 | 
					                                               preferences.detour_tolerance_minute)
 | 
				
			||||||
    except TimeoutError as te :
 | 
					 | 
				
			||||||
        logger.error(f'Refiner failed : {str(te)} Using base tour.')
 | 
					 | 
				
			||||||
        refined_tour = base_tour
 | 
					 | 
				
			||||||
    except Exception as exc :
 | 
					    except Exception as exc :
 | 
				
			||||||
        raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(exc)}") from exc
 | 
					        logger.warning(f"Refiner failed. Proceeding with base trip {str(exc)}")
 | 
				
			||||||
 | 
					        refined_tour = base_tour
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    t_second_stage = time.time() - start_time
 | 
					    t_second_stage = time.time() - start_time
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -158,6 +163,7 @@ def get_trip(trip_uuid: str) -> Trip:
 | 
				
			|||||||
        trip = cache_client.get(f"trip_{trip_uuid}")
 | 
					        trip = cache_client.get(f"trip_{trip_uuid}")
 | 
				
			||||||
        return trip
 | 
					        return trip
 | 
				
			||||||
    except KeyError as exc:
 | 
					    except KeyError as exc:
 | 
				
			||||||
 | 
					        logger.error(f"Failed to fetch trip with UUID {trip_uuid}: {str(exc)}")
 | 
				
			||||||
        raise HTTPException(status_code=404, detail="Trip not found") from exc
 | 
					        raise HTTPException(status_code=404, detail="Trip not found") from exc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -176,6 +182,7 @@ def get_landmark(landmark_uuid: str) -> Landmark:
 | 
				
			|||||||
        landmark = cache_client.get(f"landmark_{landmark_uuid}")
 | 
					        landmark = cache_client.get(f"landmark_{landmark_uuid}")
 | 
				
			||||||
        return landmark
 | 
					        return landmark
 | 
				
			||||||
    except KeyError as exc:
 | 
					    except KeyError as exc:
 | 
				
			||||||
 | 
					        logger.error(f"Failed to fetch landmark with UUID {landmark_uuid}: {str(exc)}")
 | 
				
			||||||
        raise HTTPException(status_code=404, detail="Landmark not found") from exc
 | 
					        raise HTTPException(status_code=404, detail="Landmark not found") from exc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -194,6 +201,7 @@ def update_trip_time(trip_uuid: str, removed_landmark_uuid: str) -> Trip:
 | 
				
			|||||||
    try:
 | 
					    try:
 | 
				
			||||||
        trip = cache_client.get(f'trip_{trip_uuid}')
 | 
					        trip = cache_client.get(f'trip_{trip_uuid}')
 | 
				
			||||||
    except KeyError as exc:
 | 
					    except KeyError as exc:
 | 
				
			||||||
 | 
					        logger.error(f"Failed to update trip with UUID {trip_uuid} (trip not found): {str(exc)}")
 | 
				
			||||||
        raise HTTPException(status_code=404, detail='Trip not found') from exc
 | 
					        raise HTTPException(status_code=404, detail='Trip not found') from exc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    landmarks = []
 | 
					    landmarks = []
 | 
				
			||||||
@@ -208,6 +216,7 @@ def update_trip_time(trip_uuid: str, removed_landmark_uuid: str) -> Trip:
 | 
				
			|||||||
                landmarks.append(landmark)
 | 
					                landmarks.append(landmark)
 | 
				
			||||||
            next_uuid = landmark.next_uuid  # Prepare for the next iteration
 | 
					            next_uuid = landmark.next_uuid  # Prepare for the next iteration
 | 
				
			||||||
    except KeyError as exc:
 | 
					    except KeyError as exc:
 | 
				
			||||||
 | 
					        logger.error(f"Failed to update trip with UUID {trip_uuid} : {str(exc)}")
 | 
				
			||||||
        raise HTTPException(status_code=404, detail=f'landmark {next_uuid} not found') from exc
 | 
					        raise HTTPException(status_code=404, detail=f'landmark {next_uuid} not found') from exc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Re-link every thing and compute times again
 | 
					    # Re-link every thing and compute times again
 | 
				
			||||||
@@ -215,32 +224,3 @@ def update_trip_time(trip_uuid: str, removed_landmark_uuid: str) -> Trip:
 | 
				
			|||||||
    trip = Trip.from_linked_landmarks(linked_tour, cache_client)
 | 
					    trip = Trip.from_linked_landmarks(linked_tour, cache_client)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return trip
 | 
					    return trip
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@app.post("/toilets/new")
 | 
					 | 
				
			||||||
def get_toilets(location: tuple[float, float] = Query(...), radius: int = 500) -> list[Toilets] :
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    Endpoint to find toilets within a specified radius from a given location.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    This endpoint expects the `location` and `radius` as **query parameters**, not in the request body.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Args:
 | 
					 | 
				
			||||||
        location (tuple[float, float]): The latitude and longitude of the location to search from.
 | 
					 | 
				
			||||||
        radius (int, optional): The radius (in meters) within which to search for toilets. Defaults to 500 meters.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Returns:
 | 
					 | 
				
			||||||
        list[Toilets]: A list of Toilets objects that meet the criteria.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    if location is None:
 | 
					 | 
				
			||||||
        raise HTTPException(status_code=406, detail="Coordinates not provided or invalid")
 | 
					 | 
				
			||||||
    if not (-90 <= location[0] <= 90 or -180 <= location[1] <= 180):
 | 
					 | 
				
			||||||
        raise HTTPException(status_code=422, detail="Start coordinates not in range")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    toilets_manager = ToiletsManager(location, radius)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try :
 | 
					 | 
				
			||||||
        toilets_list = toilets_manager.generate_toilet_list()
 | 
					 | 
				
			||||||
        return toilets_list
 | 
					 | 
				
			||||||
    except KeyError as exc:
 | 
					 | 
				
			||||||
        raise HTTPException(status_code=404, detail="No toilets found") from exc
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -257,7 +257,6 @@ class Optimizer:
 | 
				
			|||||||
        Returns:
 | 
					        Returns:
 | 
				
			||||||
            None: This function modifies the `prob` object by adding L-2 equality constraints in-place.
 | 
					            None: This function modifies the `prob` object by adding L-2 equality constraints in-place.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        # FIXME: weird 0 artifact in the coefficients popping up
 | 
					 | 
				
			||||||
        # Loop through rows 1 to L-2 to prevent stacked ones
 | 
					        # Loop through rows 1 to L-2 to prevent stacked ones
 | 
				
			||||||
        for i in range(1, L-1):
 | 
					        for i in range(1, L-1):
 | 
				
			||||||
            # Add the constraint that sums across each "row" or "block" in the decision variables
 | 
					            # Add the constraint that sums across each "row" or "block" in the decision variables
 | 
				
			||||||
@@ -590,7 +589,7 @@ class Optimizer:
 | 
				
			|||||||
        try :
 | 
					        try :
 | 
				
			||||||
            prob.solve(pl.PULP_CBC_CMD(msg=False, timeLimit=self.time_limit+1, gapRel=self.gap_rel))
 | 
					            prob.solve(pl.PULP_CBC_CMD(msg=False, timeLimit=self.time_limit+1, gapRel=self.gap_rel))
 | 
				
			||||||
        except Exception as exc :
 | 
					        except Exception as exc :
 | 
				
			||||||
            raise Exception(f"No solution found: {exc}") from exc
 | 
					            raise Exception(f"No solution found: {str(exc)}") from exc
 | 
				
			||||||
        status = pl.LpStatus[prob.status]
 | 
					        status = pl.LpStatus[prob.status]
 | 
				
			||||||
        solution = [pl.value(var) for var in x]  # The values of the decision variables (will be 0 or 1)
 | 
					        solution = [pl.value(var) for var in x]  # The values of the decision variables (will be 0 or 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -598,7 +597,7 @@ class Optimizer:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        # Raise error if no solution is found. FIXME: for now this throws the internal server error
 | 
					        # Raise error if no solution is found. FIXME: for now this throws the internal server error
 | 
				
			||||||
        if status != 'Optimal' :
 | 
					        if status != 'Optimal' :
 | 
				
			||||||
            self.logger.error("The problem is overconstrained, no solution on first try.")
 | 
					            self.logger.warning("The problem is overconstrained, no solution on first try.")
 | 
				
			||||||
            raise ArithmeticError("No solution could be found. Please try again with more time or different preferences.")
 | 
					            raise ArithmeticError("No solution could be found. Please try again with more time or different preferences.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # If there is a solution, we're good to go, just check for connectiveness
 | 
					        # If there is a solution, we're good to go, just check for connectiveness
 | 
				
			||||||
@@ -608,7 +607,7 @@ class Optimizer:
 | 
				
			|||||||
        while circles is not None :
 | 
					        while circles is not None :
 | 
				
			||||||
            i += 1
 | 
					            i += 1
 | 
				
			||||||
            if i == self.max_iter :
 | 
					            if i == self.max_iter :
 | 
				
			||||||
                self.logger.error(f'Timeout: No solution found after {self.max_iter} iterations.')
 | 
					                self.logger.warning(f'Timeout: No solution found after {self.max_iter} iterations.')
 | 
				
			||||||
                raise TimeoutError(f"Optimization took too long. No solution found after {self.max_iter} iterations.")
 | 
					                raise TimeoutError(f"Optimization took too long. No solution found after {self.max_iter} iterations.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            for circle in circles :
 | 
					            for circle in circles :
 | 
				
			||||||
@@ -618,12 +617,13 @@ class Optimizer:
 | 
				
			|||||||
            try :
 | 
					            try :
 | 
				
			||||||
                prob.solve(pl.PULP_CBC_CMD(msg=False, timeLimit=self.time_limit, gapRel=self.gap_rel))
 | 
					                prob.solve(pl.PULP_CBC_CMD(msg=False, timeLimit=self.time_limit, gapRel=self.gap_rel))
 | 
				
			||||||
            except Exception as exc :
 | 
					            except Exception as exc :
 | 
				
			||||||
                raise Exception(f"No solution found: {exc}") from exc
 | 
					                self.logger.warning("No solution found: {str(exc)")
 | 
				
			||||||
 | 
					                raise Exception(f"No solution found: {str(exc)}") from exc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            solution = [pl.value(var) for var in x]
 | 
					            solution = [pl.value(var) for var in x]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if pl.LpStatus[prob.status] != 'Optimal' :
 | 
					            if pl.LpStatus[prob.status] != 'Optimal' :
 | 
				
			||||||
                self.logger.error("The problem is overconstrained, no solution after {i} cycles.")
 | 
					                self.logger.warning("The problem is overconstrained, no solution after {i} cycles.")
 | 
				
			||||||
                raise ArithmeticError("No solution could be found. Please try again with more time or different preferences.")
 | 
					                raise ArithmeticError("No solution could be found. Please try again with more time or different preferences.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            circles = self.is_connected(solution)
 | 
					            circles = self.is_connected(solution)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -278,7 +278,7 @@ class Refiner :
 | 
				
			|||||||
            better_tour_poly = concave_hull(MultiPoint(coords))  # Create concave hull with "core" of tour leaving out start and finish
 | 
					            better_tour_poly = concave_hull(MultiPoint(coords))  # Create concave hull with "core" of tour leaving out start and finish
 | 
				
			||||||
            xs, ys = better_tour_poly.exterior.xy
 | 
					            xs, ys = better_tour_poly.exterior.xy
 | 
				
			||||||
            """ 
 | 
					            """ 
 | 
				
			||||||
            ERROR HERE : 
 | 
					            FIXED : ERROR HERE : 
 | 
				
			||||||
                Exception has occurred: AttributeError
 | 
					                Exception has occurred: AttributeError
 | 
				
			||||||
                'LineString' object has no attribute 'exterior'
 | 
					                'LineString' object has no attribute 'exterior'
 | 
				
			||||||
            """
 | 
					            """
 | 
				
			||||||
@@ -356,7 +356,7 @@ class Refiner :
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        # If unsuccessful optimization, use the base_tour.
 | 
					        # If unsuccessful optimization, use the base_tour.
 | 
				
			||||||
        if new_tour is None:
 | 
					        if new_tour is None:
 | 
				
			||||||
            self.logger.warning("No solution found for the refined tour. Returning the initial tour.")
 | 
					            self.logger.warning("Refiner failed: No solution found during second stage optimization.")
 | 
				
			||||||
            new_tour = base_tour
 | 
					            new_tour = base_tour
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # If only one landmark, return it.
 | 
					        # If only one landmark, return it.
 | 
				
			||||||
@@ -369,6 +369,7 @@ class Refiner :
 | 
				
			|||||||
        # Fix the tour using Polygons if the path looks weird.
 | 
					        # Fix the tour using Polygons if the path looks weird.
 | 
				
			||||||
        # Conditions : circular trip and invalid polygon.
 | 
					        # Conditions : circular trip and invalid polygon.
 | 
				
			||||||
        if base_tour[0].location == base_tour[-1].location and not better_poly.is_valid :
 | 
					        if base_tour[0].location == base_tour[-1].location and not better_poly.is_valid :
 | 
				
			||||||
 | 
					            self.logger.debug("Tours might be funky, attempting to correct with polygons")
 | 
				
			||||||
            better_tour = self.fix_using_polygon(better_tour)
 | 
					            better_tour = self.fix_using_polygon(better_tour)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return better_tour
 | 
					        return better_tour
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					"""Module defining the handling of cache data from Overpass requests."""
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import json
 | 
					import json
 | 
				
			||||||
import hashlib
 | 
					import hashlib
 | 
				
			||||||
@@ -61,7 +62,7 @@ class JSONCache(CachingStrategyBase):
 | 
				
			|||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def set(self, key, value):
 | 
					    def set(self, key, value):
 | 
				
			||||||
        """Save the JSON data as an ElementTree to the cache."""
 | 
					        """Save the JSON data in the cache."""
 | 
				
			||||||
        filename = self._filename(key)
 | 
					        filename = self._filename(key)
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            # Write the JSON data to the cache file
 | 
					            # Write the JSON data to the cache file
 | 
				
			||||||
@@ -94,7 +95,7 @@ class JSONCache(CachingStrategyBase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def close(self):
 | 
					    def close(self):
 | 
				
			||||||
        """Cleanup method, if needed."""
 | 
					        """Cleanup method, if needed."""
 | 
				
			||||||
        pass
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CachingStrategy:
 | 
					class CachingStrategy:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
@@ -107,6 +108,7 @@ class CachingStrategy:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def use(cls, strategy_name='JSON', **kwargs):
 | 
					    def use(cls, strategy_name='JSON', **kwargs):
 | 
				
			||||||
 | 
					        """Define the caching strategy to use."""
 | 
				
			||||||
        if cls.__strategy:
 | 
					        if cls.__strategy:
 | 
				
			||||||
            cls.__strategy.close()
 | 
					            cls.__strategy.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -119,10 +121,12 @@ class CachingStrategy:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def get(cls, key):
 | 
					    def get(cls, key):
 | 
				
			||||||
 | 
					        """Get the data from the cache."""
 | 
				
			||||||
        return cls.__strategy.get(key)
 | 
					        return cls.__strategy.get(key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def set(cls, key, value):
 | 
					    def set(cls, key, value):
 | 
				
			||||||
 | 
					        """Save the data in the cache."""
 | 
				
			||||||
        cls.__strategy.set(key, value)
 | 
					        cls.__strategy.set(key, value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
"""Module allowing connexion to overpass api and fectch data from OSM."""
 | 
					"""Module allowing connexion to overpass api and fectch data from OSM."""
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
 | 
					import time
 | 
				
			||||||
import urllib
 | 
					import urllib
 | 
				
			||||||
import math
 | 
					import math
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
@@ -59,13 +60,11 @@ class Overpass :
 | 
				
			|||||||
            return Overpass._filter_landmarks(cached_responses, bbox)
 | 
					            return Overpass._filter_landmarks(cached_responses, bbox)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # If there is no cached data, fetch all from Overpass.
 | 
					        # If there is no cached data, fetch all from Overpass.
 | 
				
			||||||
        elif not cached_responses :
 | 
					        if not cached_responses :
 | 
				
			||||||
            query_str = Overpass.build_query(bbox, osm_types, selector, conditions, out)
 | 
					            query_str = Overpass.build_query(bbox, osm_types, selector, conditions, out)
 | 
				
			||||||
            self.logger.debug(f'Query string: {query_str}')
 | 
					            self.logger.debug(f'Query string: {query_str}')
 | 
				
			||||||
            return self.fetch_data_from_api(query_str)
 | 
					            return self.fetch_data_from_api(query_str)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Hybrid cache: some data from Overpass, some data from cache.
 | 
					 | 
				
			||||||
        else :
 | 
					 | 
				
			||||||
        # Resize the bbox for smaller search area and build new query string.
 | 
					        # Resize the bbox for smaller search area and build new query string.
 | 
				
			||||||
        non_cached_bbox = Overpass._get_non_cached_bbox(non_cached_cells, bbox)
 | 
					        non_cached_bbox = Overpass._get_non_cached_bbox(non_cached_cells, bbox)
 | 
				
			||||||
        query_str = Overpass.build_query(non_cached_bbox, osm_types, selector, conditions, out)
 | 
					        query_str = Overpass.build_query(non_cached_bbox, osm_types, selector, conditions, out)
 | 
				
			||||||
@@ -96,9 +95,10 @@ class Overpass :
 | 
				
			|||||||
                return elements
 | 
					                return elements
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        except urllib.error.URLError as e:
 | 
					        except urllib.error.URLError as e:
 | 
				
			||||||
            self.logger.error(f"Error connecting to Overpass API: {e}")
 | 
					            self.logger.error(f"Error connecting to Overpass API: {str(e)}")
 | 
				
			||||||
            raise ConnectionError(f"Error connecting to Overpass API: {e}") from e
 | 
					            raise ConnectionError(f"Error connecting to Overpass API: {str(e)}") from e
 | 
				
			||||||
        except Exception as exc :
 | 
					        except Exception as exc :
 | 
				
			||||||
 | 
					            self.logger.error(f"unexpected error while fetching data from Overpass: {str(exc)}")
 | 
				
			||||||
            raise Exception(f'An unexpected error occured: {str(exc)}') from exc
 | 
					            raise Exception(f'An unexpected error occured: {str(exc)}') from exc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -122,7 +122,7 @@ class Overpass :
 | 
				
			|||||||
                self.caching_strategy.set(cache_key, elements)
 | 
					                self.caching_strategy.set(cache_key, elements)
 | 
				
			||||||
                self.logger.debug(f'Cache set for {cache_key}')
 | 
					                self.logger.debug(f'Cache set for {cache_key}')
 | 
				
			||||||
        except urllib.error.URLError as e:
 | 
					        except urllib.error.URLError as e:
 | 
				
			||||||
            raise ConnectionError(f"Error connecting to Overpass API: {e}") from e
 | 
					            raise ConnectionError(f"Error connecting to Overpass API: {str(e)}") from e
 | 
				
			||||||
        except Exception as exc :
 | 
					        except Exception as exc :
 | 
				
			||||||
            raise Exception(f'An unexpected error occured: {str(exc)}') from exc
 | 
					            raise Exception(f'An unexpected error occured: {str(exc)}') from exc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -153,7 +153,7 @@ class Overpass :
 | 
				
			|||||||
            - If no conditions are provided, the query will just use the `selector` to filter the OSM 
 | 
					            - If no conditions are provided, the query will just use the `selector` to filter the OSM 
 | 
				
			||||||
            elements without additional constraints.
 | 
					            elements without additional constraints.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        query = '[out:json];('
 | 
					        query = '[out:json][timeout:20];('
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # convert the bbox to string.
 | 
					        # convert the bbox to string.
 | 
				
			||||||
        bbox_str = f"({','.join(map(str, bbox))})"
 | 
					        bbox_str = f"({','.join(map(str, bbox))})"
 | 
				
			||||||
@@ -388,7 +388,7 @@ def get_base_info(elem: dict, osm_type: OSM_TYPES, with_name=False) :
 | 
				
			|||||||
    if with_name :
 | 
					    if with_name :
 | 
				
			||||||
        name = elem.get('tags', {}).get('name')
 | 
					        name = elem.get('tags', {}).get('name')
 | 
				
			||||||
        return osm_id, coords, name
 | 
					        return osm_id, coords, name
 | 
				
			||||||
    else :
 | 
					
 | 
				
			||||||
    return osm_id, coords
 | 
					    return osm_id, coords
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -399,18 +399,25 @@ def fill_cache():
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    overpass = Overpass()
 | 
					    overpass = Overpass()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    n_files = 0
 | 
				
			||||||
 | 
					    total = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with os.scandir(OSM_CACHE_DIR) as it:
 | 
					    with os.scandir(OSM_CACHE_DIR) as it:
 | 
				
			||||||
        for entry in it:
 | 
					        for entry in it:
 | 
				
			||||||
            if entry.is_file() and entry.name.startswith('hollow_'):
 | 
					            if entry.is_file() and entry.name.startswith('hollow_'):
 | 
				
			||||||
 | 
					                total += 1
 | 
				
			||||||
                try :
 | 
					                try :
 | 
				
			||||||
                    # Read the whole file content as a string
 | 
					                    # Read the whole file content as a string
 | 
				
			||||||
                    with open(entry.path, 'r') as f:
 | 
					                    with open(entry.path, 'r', encoding='utf-8') as f:
 | 
				
			||||||
                        # load data and fill the cache with the query and key
 | 
					                        # load data and fill the cache with the query and key
 | 
				
			||||||
                        json_data = json.load(f)
 | 
					                        json_data = json.load(f)
 | 
				
			||||||
                        overpass.fill_cache(json_data)
 | 
					                        overpass.fill_cache(json_data)
 | 
				
			||||||
 | 
					                        n_files += 1
 | 
				
			||||||
 | 
					                        time.sleep(1)
 | 
				
			||||||
                    # Now delete the file as the cache is filled
 | 
					                    # Now delete the file as the cache is filled
 | 
				
			||||||
                    os.remove(entry.path)
 | 
					                    os.remove(entry.path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                except Exception as exc :
 | 
					                except Exception as exc :
 | 
				
			||||||
                    overpass.logger.error(f'An error occured while parsing file {entry.path} as .json file')
 | 
					                    overpass.logger.error(f'An error occured while parsing file {entry.path} as .json file: {str(exc)}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    overpass.logger.info(f"Successfully filled {n_files}/{total} cache files.")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -72,6 +72,7 @@ sightseeing:
 | 
				
			|||||||
    # - castle
 | 
					    # - castle
 | 
				
			||||||
    # - museum
 | 
					    # - museum
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
museums:
 | 
					museums:
 | 
				
			||||||
  tourism:
 | 
					  tourism:
 | 
				
			||||||
    - museum
 | 
					    - museum
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,4 +6,4 @@ max_landmarks_refiner: 20
 | 
				
			|||||||
overshoot: 0.0016
 | 
					overshoot: 0.0016
 | 
				
			||||||
time_limit: 1
 | 
					time_limit: 1
 | 
				
			||||||
gap_rel: 0.025
 | 
					gap_rel: 0.025
 | 
				
			||||||
max_iter: 40
 | 
					max_iter: 80
 | 
				
			||||||
@@ -1,8 +1,7 @@
 | 
				
			|||||||
"""Definition of the Landmark class to handle visitable objects across the world."""
 | 
					"""Definition of the Landmark class to handle visitable objects across the world."""
 | 
				
			||||||
 | 
					 | 
				
			||||||
from typing import Optional, Literal
 | 
					from typing import Optional, Literal
 | 
				
			||||||
from uuid import uuid4, UUID
 | 
					from uuid import uuid4, UUID
 | 
				
			||||||
from pydantic import BaseModel, ConfigDict, Field
 | 
					from pydantic import BaseModel, Field
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Output to frontend
 | 
					# Output to frontend
 | 
				
			||||||
@@ -50,7 +49,8 @@ class Landmark(BaseModel) :
 | 
				
			|||||||
    image_url : Optional[str] = None
 | 
					    image_url : Optional[str] = None
 | 
				
			||||||
    website_url : Optional[str] = None
 | 
					    website_url : Optional[str] = None
 | 
				
			||||||
    wiki_url : Optional[str] = None
 | 
					    wiki_url : Optional[str] = None
 | 
				
			||||||
    description : Optional[str] = None                          # TODO future
 | 
					    keywords: Optional[dict] = {}
 | 
				
			||||||
 | 
					    description : Optional[str] = None
 | 
				
			||||||
    duration : Optional[int] = 5
 | 
					    duration : Optional[int] = 5
 | 
				
			||||||
    name_en : Optional[str] = None
 | 
					    name_en : Optional[str] = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -69,6 +69,7 @@ class Landmark(BaseModel) :
 | 
				
			|||||||
    is_viewpoint : Optional[bool] = False
 | 
					    is_viewpoint : Optional[bool] = False
 | 
				
			||||||
    is_place_of_worship : Optional[bool] = False
 | 
					    is_place_of_worship : Optional[bool] = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self) -> str:
 | 
					    def __str__(self) -> str:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        String representation of the Landmark object.
 | 
					        String representation of the Landmark object.
 | 
				
			||||||
@@ -122,26 +123,3 @@ class Landmark(BaseModel) :
 | 
				
			|||||||
        return (self.uuid == value.uuid or
 | 
					        return (self.uuid == value.uuid or
 | 
				
			||||||
                self.osm_id == value.osm_id or
 | 
					                self.osm_id == value.osm_id or
 | 
				
			||||||
                (self.name == value.name and self.distance(value) < 0.001))
 | 
					                (self.name == value.name and self.distance(value) < 0.001))
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Toilets(BaseModel) :
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    Model for toilets. When false/empty the information is either false either not known.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    location : tuple
 | 
					 | 
				
			||||||
    wheelchair : Optional[bool] = False
 | 
					 | 
				
			||||||
    changing_table : Optional[bool] = False
 | 
					 | 
				
			||||||
    fee : Optional[bool] = False
 | 
					 | 
				
			||||||
    opening_hours : Optional[str] = ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __str__(self) -> str:
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        String representation of the Toilets object.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Returns:
 | 
					 | 
				
			||||||
            str: A formatted string with the toilets location.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return f'Toilets @{self.location}'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    model_config = ConfigDict(from_attributes=True)
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										26
									
								
								backend/src/structs/toilets.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								backend/src/structs/toilets.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					"""Definition of the Toilets class."""
 | 
				
			||||||
 | 
					from typing import Optional
 | 
				
			||||||
 | 
					from pydantic import BaseModel, ConfigDict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Toilets(BaseModel) :
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Model for toilets. When false/empty the information is either false either not known.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    location : tuple
 | 
				
			||||||
 | 
					    wheelchair : Optional[bool] = False
 | 
				
			||||||
 | 
					    changing_table : Optional[bool] = False
 | 
				
			||||||
 | 
					    fee : Optional[bool] = False
 | 
				
			||||||
 | 
					    opening_hours : Optional[str] = ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self) -> str:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        String representation of the Toilets object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            str: A formatted string with the toilets location.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return f'Toilets @{self.location}'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    model_config = ConfigDict(from_attributes=True)
 | 
				
			||||||
@@ -46,8 +46,6 @@ def test_turckheim(client, request):    # pylint: disable=redefined-outer-name
 | 
				
			|||||||
    # Add details to report
 | 
					    # Add details to report
 | 
				
			||||||
    log_trip_details(request, landmarks, result['total_time'], duration_minutes)
 | 
					    log_trip_details(request, landmarks, result['total_time'], duration_minutes)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # for elem in landmarks :
 | 
					 | 
				
			||||||
    #     print(elem)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # checks :
 | 
					    # checks :
 | 
				
			||||||
    assert response.status_code == 200  # check for successful planning
 | 
					    assert response.status_code == 200  # check for successful planning
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@
 | 
				
			|||||||
from fastapi.testclient import TestClient
 | 
					from fastapi.testclient import TestClient
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ..structs.landmark import Toilets
 | 
					from ..structs.toilets import Toilets
 | 
				
			||||||
from ..main import app
 | 
					from ..main import app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,6 @@
 | 
				
			|||||||
"""Helper methods for testing."""
 | 
					"""Helper methods for testing."""
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
from fastapi import HTTPException
 | 
					from fastapi import HTTPException
 | 
				
			||||||
from pydantic import ValidationError
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ..structs.landmark import Landmark
 | 
					from ..structs.landmark import Landmark
 | 
				
			||||||
from ..cache import client as cache_client
 | 
					from ..cache import client as cache_client
 | 
				
			||||||
@@ -39,7 +38,7 @@ def fetch_landmark(landmark_uuid: str):
 | 
				
			|||||||
    try:
 | 
					    try:
 | 
				
			||||||
        landmark = cache_client.get(f'landmark_{landmark_uuid}')
 | 
					        landmark = cache_client.get(f'landmark_{landmark_uuid}')
 | 
				
			||||||
        if not landmark :
 | 
					        if not landmark :
 | 
				
			||||||
            logger.warning(f'Cache miss for landmark UUID: {landmark_uuid}')
 | 
					            logger.error(f'Cache miss for landmark UUID: {landmark_uuid}')
 | 
				
			||||||
            raise HTTPException(status_code=404, detail=f'Landmark with UUID {landmark_uuid} not found in cache.')
 | 
					            raise HTTPException(status_code=404, detail=f'Landmark with UUID {landmark_uuid} not found in cache.')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Validate that the fetched data is a dictionary
 | 
					        # Validate that the fetched data is a dictionary
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										0
									
								
								backend/src/toilets/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								backend/src/toilets/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										38
									
								
								backend/src/toilets/toilet_routes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								backend/src/toilets/toilet_routes.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					"""Defines the endpoint for fetching toilet locations."""
 | 
				
			||||||
 | 
					from fastapi import HTTPException, APIRouter, Query
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from ..structs.toilets import Toilets
 | 
				
			||||||
 | 
					from .toilets_manager import ToiletsManager
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Define the API router
 | 
				
			||||||
 | 
					router = APIRouter()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@router.post("/toilets/new")
 | 
				
			||||||
 | 
					def get_toilets(location: tuple[float, float] = Query(...), radius: int = 500) -> list[Toilets] :
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Endpoint to find toilets within a specified radius from a given location.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    This endpoint expects the `location` and `radius` as **query parameters**, not in the request body.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        location (tuple[float, float]): The latitude and longitude of the location to search from.
 | 
				
			||||||
 | 
					        radius (int, optional): The radius (in meters) within which to search for toilets. Defaults to 500 meters.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        list[Toilets]: A list of Toilets objects that meet the criteria.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if location is None:
 | 
				
			||||||
 | 
					        raise HTTPException(status_code=406, detail="Coordinates not provided or invalid")
 | 
				
			||||||
 | 
					    if not (-90 <= location[0] <= 90 or -180 <= location[1] <= 180):
 | 
				
			||||||
 | 
					        raise HTTPException(status_code=422, detail="Start coordinates not in range")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    toilets_manager = ToiletsManager(location, radius)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try :
 | 
				
			||||||
 | 
					        toilets_list = toilets_manager.generate_toilet_list()
 | 
				
			||||||
 | 
					    except KeyError as exc:
 | 
				
			||||||
 | 
					        raise HTTPException(status_code=404, detail="No toilets found") from exc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return toilets_list
 | 
				
			||||||
@@ -2,8 +2,8 @@
 | 
				
			|||||||
import logging
 | 
					import logging
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ..overpass.overpass import Overpass, get_base_info
 | 
					from ..overpass.overpass import Overpass, get_base_info
 | 
				
			||||||
from ..structs.landmark import Toilets
 | 
					from ..structs.toilets import Toilets
 | 
				
			||||||
from .utils import create_bbox
 | 
					from ..utils.bbox import create_bbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# silence the overpass logger
 | 
					# silence the overpass logger
 | 
				
			||||||
@@ -65,7 +65,7 @@ class ToiletsManager:
 | 
				
			|||||||
        try:
 | 
					        try:
 | 
				
			||||||
            result = self.overpass.fetch_data_from_api(query_str=query)
 | 
					            result = self.overpass.fetch_data_from_api(query_str=query)
 | 
				
			||||||
        except Exception as e:
 | 
					        except Exception as e:
 | 
				
			||||||
            self.logger.error(f"Error fetching landmarks: {e}")
 | 
					            self.logger.error(f"Error fetching toilets: {e}")
 | 
				
			||||||
            return None
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        toilets_list = self.to_toilets(result)
 | 
					        toilets_list = self.to_toilets(result)
 | 
				
			||||||
							
								
								
									
										48
									
								
								status
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								status
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					error: wrong number of arguments, should be from 1 to 2
 | 
				
			||||||
 | 
					usage: git config [<options>]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Config file location
 | 
				
			||||||
 | 
					    --[no-]global         use global config file
 | 
				
			||||||
 | 
					    --[no-]system         use system config file
 | 
				
			||||||
 | 
					    --[no-]local          use repository config file
 | 
				
			||||||
 | 
					    --[no-]worktree       use per-worktree config file
 | 
				
			||||||
 | 
					    -f, --[no-]file <file>
 | 
				
			||||||
 | 
					                          use given config file
 | 
				
			||||||
 | 
					    --[no-]blob <blob-id> read config from given blob object
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Action
 | 
				
			||||||
 | 
					    --[no-]get            get value: name [value-pattern]
 | 
				
			||||||
 | 
					    --[no-]get-all        get all values: key [value-pattern]
 | 
				
			||||||
 | 
					    --[no-]get-regexp     get values for regexp: name-regex [value-pattern]
 | 
				
			||||||
 | 
					    --[no-]get-urlmatch   get value specific for the URL: section[.var] URL
 | 
				
			||||||
 | 
					    --[no-]replace-all    replace all matching variables: name value [value-pattern]
 | 
				
			||||||
 | 
					    --[no-]add            add a new variable: name value
 | 
				
			||||||
 | 
					    --[no-]unset          remove a variable: name [value-pattern]
 | 
				
			||||||
 | 
					    --[no-]unset-all      remove all matches: name [value-pattern]
 | 
				
			||||||
 | 
					    --[no-]rename-section rename section: old-name new-name
 | 
				
			||||||
 | 
					    --[no-]remove-section remove a section: name
 | 
				
			||||||
 | 
					    -l, --[no-]list       list all
 | 
				
			||||||
 | 
					    --[no-]fixed-value    use string equality when comparing values to 'value-pattern'
 | 
				
			||||||
 | 
					    -e, --[no-]edit       open an editor
 | 
				
			||||||
 | 
					    --[no-]get-color      find the color configured: slot [default]
 | 
				
			||||||
 | 
					    --[no-]get-colorbool  find the color setting: slot [stdout-is-tty]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Type
 | 
				
			||||||
 | 
					    -t, --[no-]type <type>
 | 
				
			||||||
 | 
					                          value is given this type
 | 
				
			||||||
 | 
					    --bool                value is "true" or "false"
 | 
				
			||||||
 | 
					    --int                 value is decimal number
 | 
				
			||||||
 | 
					    --bool-or-int         value is --bool or --int
 | 
				
			||||||
 | 
					    --bool-or-str         value is --bool or string
 | 
				
			||||||
 | 
					    --path                value is a path (file or directory name)
 | 
				
			||||||
 | 
					    --expiry-date         value is an expiry date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Other
 | 
				
			||||||
 | 
					    -z, --[no-]null       terminate values with NUL byte
 | 
				
			||||||
 | 
					    --[no-]name-only      show variable names only
 | 
				
			||||||
 | 
					    --[no-]includes       respect include directives on lookup
 | 
				
			||||||
 | 
					    --[no-]show-origin    show origin of config (file, standard input, blob, command line)
 | 
				
			||||||
 | 
					    --[no-]show-scope     show scope of config (worktree, local, global, system, command)
 | 
				
			||||||
 | 
					    --[no-]default <value>
 | 
				
			||||||
 | 
					                          with --get, use default value when missing entry
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Reference in New Issue
	
	Block a user