Adding features to find public toilets and shopping streets #41
| @@ -30,5 +30,5 @@ jobs: | |||||||
|       working-directory: backend |       working-directory: backend | ||||||
|  |  | ||||||
|     - name: Run linter |     - name: Run linter | ||||||
|       run: pipenv run pylint src |       run: pipenv run pylint src --fail-under=9 | ||||||
|       working-directory: backend |       working-directory: backend | ||||||
|   | |||||||
| @@ -439,7 +439,8 @@ disable=raw-checker-failed, | |||||||
|         use-symbolic-message-instead, |         use-symbolic-message-instead, | ||||||
|         use-implicit-booleaness-not-comparison-to-string, |         use-implicit-booleaness-not-comparison-to-string, | ||||||
|         use-implicit-booleaness-not-comparison-to-zero, |         use-implicit-booleaness-not-comparison-to-zero, | ||||||
|         import-error |         import-error, | ||||||
|  |         line-too-long | ||||||
|  |  | ||||||
| # Enable the message, report, category or checker with the given id(s). You can | # Enable the message, report, category or checker with the given id(s). You can | ||||||
| # either give multiple identifier separated by comma (,) or put this option | # either give multiple identifier separated by comma (,) or put this option | ||||||
|   | |||||||
| @@ -23,3 +23,5 @@ osmpythontools = "*" | |||||||
| pywikibot = "*" | pywikibot = "*" | ||||||
| pymemcache = "*" | pymemcache = "*" | ||||||
| fastapi-cli = "*" | fastapi-cli = "*" | ||||||
|  | scikit-learn = "*" | ||||||
|  | pyqt6 = "*" | ||||||
|   | |||||||
							
								
								
									
										609
									
								
								backend/Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										609
									
								
								backend/Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|     "_meta": { |     "_meta": { | ||||||
|         "hash": { |         "hash": { | ||||||
|             "sha256": "0af258788e2a8ae214f9af013cd7a7b19fb3adb941d4ea1de23902daf9062f5a" |             "sha256": "bb22b4e28c7aa199c94b688ad93d3ab0ccf1089a172131f4aec03b78e7bd7f1c" | ||||||
|         }, |         }, | ||||||
|         "pipfile-spec": 6, |         "pipfile-spec": 6, | ||||||
|         "requires": {}, |         "requires": {}, | ||||||
| @@ -379,6 +379,14 @@ | |||||||
|             "markers": "python_version >= '3.6'", |             "markers": "python_version >= '3.6'", | ||||||
|             "version": "==3.10" |             "version": "==3.10" | ||||||
|         }, |         }, | ||||||
|  |         "joblib": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6", | ||||||
|  |                 "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e" | ||||||
|  |             ], | ||||||
|  |             "markers": "python_version >= '3.8'", | ||||||
|  |             "version": "==1.4.2" | ||||||
|  |         }, | ||||||
|         "kiwisolver": { |         "kiwisolver": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a", |                 "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a", | ||||||
| @@ -653,49 +661,50 @@ | |||||||
|         }, |         }, | ||||||
|         "matplotlib": { |         "matplotlib": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:039082812cacd6c6bec8e17a9c1e6baca230d4116d522e81e1f63a74d01d2e21", |                 "sha256:026bdf3137ab6022c866efa4813b6bbeddc2ed4c9e7e02f0e323a7bca380dfa0", | ||||||
|                 "sha256:03ba9c1299c920964e8d3857ba27173b4dbb51ca4bab47ffc2c2ba0eb5e2cbc5", |                 "sha256:031b7f5b8e595cc07def77ec5b58464e9bb67dc5760be5d6f26d9da24892481d", | ||||||
|                 "sha256:050598c2b29e0b9832cde72bcf97627bf00262adbc4a54e2b856426bb2ef0697", |                 "sha256:0a0a63cb8404d1d1f94968ef35738900038137dab8af836b6c21bb6f03d75465", | ||||||
|                 "sha256:18128cc08f0d3cfff10b76baa2f296fc28c4607368a8402de61bb3f2eb33c7d9", |                 "sha256:0a361bd5583bf0bcc08841df3c10269617ee2a36b99ac39d455a767da908bbbc", | ||||||
|                 "sha256:1cd93b91ab47a3616b4d3c42b52f8363b88ca021e340804c6ab2536344fad9ca", |                 "sha256:10d3e5c7a99bd28afb957e1ae661323b0800d75b419f24d041ed1cc5d844a764", | ||||||
|                 "sha256:1d94ff717eb2bd0b58fe66380bd8b14ac35f48a98e7c6765117fe67fb7684e64", |                 "sha256:1c40c244221a1adbb1256692b1133c6fb89418df27bf759a31a333e7912a4010", | ||||||
|                 "sha256:306c8dfc73239f0e72ac50e5a9cf19cc4e8e331dd0c54f5e69ca8758550f1e1e", |                 "sha256:203d18df84f5288973b2d56de63d4678cc748250026ca9e1ad8f8a0fd8a75d83", | ||||||
|                 "sha256:37e51dd1c2db16ede9cfd7b5cabdfc818b2c6397c83f8b10e0e797501c963a03", |                 "sha256:213d6dc25ce686516208d8a3e91120c6a4fdae4a3e06b8505ced5b716b50cc04", | ||||||
|                 "sha256:3fd595f34aa8a55b7fc8bf9ebea8aa665a84c82d275190a61118d33fbc82ccae", |                 "sha256:3119b2f16de7f7b9212ba76d8fe6a0e9f90b27a1e04683cd89833a991682f639", | ||||||
|                 "sha256:4876d7d40219e8ae8bb70f9263bcbe5714415acfdf781086601211335e24f8aa", |                 "sha256:3fb0b37c896172899a4a93d9442ffdc6f870165f59e05ce2e07c6fded1c15749", | ||||||
|                 "sha256:5413401594cfaff0052f9d8b1aafc6d305b4bd7c4331dccd18f561ff7e1d3bd3", |                 "sha256:41b016e3be4e740b66c79a031a0a6e145728dbc248142e751e8dab4f3188ca1d", | ||||||
|                 "sha256:5816b1e1fe8c192cbc013f8f3e3368ac56fbecf02fb41b8f8559303f24c5015e", |                 "sha256:4a8d279f78844aad213c4935c18f8292a9432d51af2d88bca99072c903948045", | ||||||
|                 "sha256:65aacf95b62272d568044531e41de26285d54aec8cb859031f511f84bd8b495a", |                 "sha256:4e6eefae6effa0c35bbbc18c25ee6e0b1da44d2359c3cd526eb0c9e703cf055d", | ||||||
|                 "sha256:6758baae2ed64f2331d4fd19be38b7b4eae3ecec210049a26b6a4f3ae1c85dcc", |                 "sha256:5f2a4ea08e6876206d511365b0bc234edc813d90b930be72c3011bbd7898796f", | ||||||
|                 "sha256:6d1ce5ed2aefcdce11904fc5bbea7d9c21fff3d5f543841edf3dea84451a09ea", |                 "sha256:66d7b171fecf96940ce069923a08ba3df33ef542de82c2ff4fe8caa8346fa95a", | ||||||
|                 "sha256:6d9f07a80deab4bb0b82858a9e9ad53d1382fd122be8cde11080f4e7dfedb38b", |                 "sha256:687df7ceff57b8f070d02b4db66f75566370e7ae182a0782b6d3d21b0d6917dc", | ||||||
|                 "sha256:7741f26a58a240f43bee74965c4882b6c93df3e7eb3de160126d8c8f53a6ae6e", |                 "sha256:6be0ba61f6ff2e6b68e4270fb63b6813c9e7dec3d15fc3a93f47480444fd72f0", | ||||||
|                 "sha256:8912ef7c2362f7193b5819d17dae8629b34a95c58603d781329712ada83f9447", |                 "sha256:6e9de2b390d253a508dd497e9b5579f3a851f208763ed67fdca5dc0c3ea6849c", | ||||||
|                 "sha256:909645cce2dc28b735674ce0931a4ac94e12f5b13f6bb0b5a5e65e7cea2c192b", |                 "sha256:760a5e89ebbb172989e8273024a1024b0f084510b9105261b3b00c15e9c9f006", | ||||||
|                 "sha256:96ab43906269ca64a6366934106fa01534454a69e471b7bf3d79083981aaab92", |                 "sha256:816a966d5d376bf24c92af8f379e78e67278833e4c7cbc9fa41872eec629a060", | ||||||
|                 "sha256:9d78bbc0cbc891ad55b4f39a48c22182e9bdaea7fc0e5dbd364f49f729ca1bbb", |                 "sha256:87ad73763d93add1b6c1f9fcd33af662fd62ed70e620c52fcb79f3ac427cf3a6", | ||||||
|                 "sha256:ab68d50c06938ef28681073327795c5db99bb4666214d2d5f880ed11aeaded66", |                 "sha256:896774766fd6be4571a43bc2fcbcb1dcca0807e53cab4a5bf88c4aa861a08e12", | ||||||
|                 "sha256:ac43031375a65c3196bee99f6001e7fa5bdfb00ddf43379d3c0609bdca042df9", |                 "sha256:8e0143975fc2a6d7136c97e19c637321288371e8f09cff2564ecd73e865ea0b9", | ||||||
|                 "sha256:ae82a14dab96fbfad7965403c643cafe6515e386de723e498cf3eeb1e0b70cc7", |                 "sha256:90a85a004fefed9e583597478420bf904bb1a065b0b0ee5b9d8d31b04b0f3f70", | ||||||
|                 "sha256:b2696efdc08648536efd4e1601b5fd491fd47f4db97a5fbfd175549a7365c1b2", |                 "sha256:9b081dac96ab19c54fd8558fac17c9d2c9cb5cc4656e7ed3261ddc927ba3e2c5", | ||||||
|                 "sha256:b82c5045cebcecd8496a4d694d43f9cc84aeeb49fe2133e036b207abe73f4d30", |                 "sha256:9d6b2e8856dec3a6db1ae51aec85c82223e834b228c1d3228aede87eee2b34f9", | ||||||
|                 "sha256:be0fc24a5e4531ae4d8e858a1a548c1fe33b176bb13eff7f9d0d38ce5112a27d", |                 "sha256:9f459c8ee2c086455744723628264e43c884be0c7d7b45d84b8cd981310b4815", | ||||||
|                 "sha256:bf81de2926c2db243c9b2cbc3917619a0fc85796c6ba4e58f541df814bbf83c7", |                 "sha256:9fa6e193c14d6944e0685cdb527cb6b38b0e4a518043e7212f214113af7391da", | ||||||
|                 "sha256:c375cc72229614632c87355366bdf2570c2dac01ac66b8ad048d2dabadf2d0d4", |                 "sha256:a42b9dc42de2cfe357efa27d9c50c7833fc5ab9b2eb7252ccd5d5f836a84e1e4", | ||||||
|                 "sha256:c797dac8bb9c7a3fd3382b16fe8f215b4cf0f22adccea36f1545a6d7be310b41", |                 "sha256:b651b0d3642991259109dc0351fc33ad44c624801367bb8307be9bfc35e427ad", | ||||||
|                 "sha256:cef2a73d06601437be399908cf13aee74e86932a5ccc6ccdf173408ebc5f6bb2", |                 "sha256:b6c12514329ac0d03128cf1dcceb335f4fbf7c11da98bca68dca8dcb983153a9", | ||||||
|                 "sha256:d52a3b618cb1cbb769ce2ee1dcdb333c3ab6e823944e9a2d36e37253815f9556", |                 "sha256:c52f48eb75fcc119a4fdb68ba83eb5f71656999420375df7c94cc68e0e14686e", | ||||||
|                 "sha256:d719465db13267bcef19ea8954a971db03b9f48b4647e3860e4bc8e6ed86610f", |                 "sha256:c96eeeb8c68b662c7747f91a385688d4b449687d29b691eff7068a4602fe6dc4", | ||||||
|                 "sha256:d8dd059447824eec055e829258ab092b56bb0579fc3164fa09c64f3acd478772", |                 "sha256:cd1077b9a09b16d8c3c7075a8add5ffbfe6a69156a57e290c800ed4d435bef1d", | ||||||
|                 "sha256:dbe196377a8248972f5cede786d4c5508ed5f5ca4a1e09b44bda889958b33f8c", |                 "sha256:cd5dbbc8e25cad5f706845c4d100e2c8b34691b412b93717ce38d8ae803bcfa5", | ||||||
|                 "sha256:e0830e188029c14e891fadd99702fd90d317df294c3298aad682739c5533721a", |                 "sha256:cf2a60daf6cecff6828bc608df00dbc794380e7234d2411c0ec612811f01969d", | ||||||
|                 "sha256:f053c40f94bc51bc03832a41b4f153d83f2062d88c72b5e79997072594e97e51", |                 "sha256:d3c93796b44fa111049b88a24105e947f03c01966b5c0cc782e2ee3887b790a3", | ||||||
|                 "sha256:f32c7410c7f246838a77d6d1eff0c0f87f3cb0e7c4247aebea71a6d5a68cab49", |                 "sha256:d796272408f8567ff7eaa00eb2856b3a00524490e47ad505b0b4ca6bb8a7411f", | ||||||
|                 "sha256:f6ee45bc4245533111ced13f1f2cace1e7f89d1c793390392a80c139d6cf0e6c", |                 "sha256:e0fcb7da73fbf67b5f4bdaa57d85bb585a4e913d4a10f3e15b32baea56a67f0a", | ||||||
|                 "sha256:f7c0410f181a531ec4e93bbc27692f2c71a15c2da16766f5ba9761e7ae518413" |                 "sha256:e14485bb1b83eeb3d55b6878f9560240981e7bbc7a8d4e1e8c38b9bd6ec8d2de", | ||||||
|  |                 "sha256:edd14cf733fdc4f6e6fe3f705af97676a7e52859bf0044aa2c84e55be739241c" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.9'", |             "markers": "python_version >= '3.9'", | ||||||
|             "version": "==3.9.2" |             "version": "==3.9.3" | ||||||
|         }, |         }, | ||||||
|         "mdurl": { |         "mdurl": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -945,118 +954,118 @@ | |||||||
|         }, |         }, | ||||||
|         "pydantic": { |         "pydantic": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0aca0f045ff6e2f097f1fe89521115335f15049eeb8a7bef3dafe4b19a74e289", |                 "sha256:2bc2d7f17232e0841cbba4641e65ba1eb6fafb3a08de3a091ff3ce14a197c4fa", | ||||||
|                 "sha256:5e7807ba9201bdf61b1b58aa6eb690916c40a47acfb114b1b4fef3e7fd5b30fc" |                 "sha256:cfb96e45951117c3024e6b67b25cdc33a3cb7b2fa62e239f7af1378358a1d99e" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "markers": "python_version >= '3.8'", |             "markers": "python_version >= '3.8'", | ||||||
|             "version": "==2.10.0" |             "version": "==2.10.2" | ||||||
|         }, |         }, | ||||||
|         "pydantic-core": { |         "pydantic-core": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0aa4d1b2eba9a325897308b3124014a142cdccb9f3e016f31d3ebee6b5ea5e75", |                 "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9", | ||||||
|                 "sha256:0d06b667e53320332be2bf6f9461f4a9b78092a079b8ce8634c9afaa7e10cd9f", |                 "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b", | ||||||
|                 "sha256:153017e3d6cd3ce979de06d84343ca424bb6092727375eba1968c8b4693c6ecb", |                 "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c", | ||||||
|                 "sha256:15e350efb67b855cd014c218716feea4986a149ed1f42a539edd271ee074a196", |                 "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529", | ||||||
|                 "sha256:185ef205256cd8b38431205698531026979db89a79587725c1e55c59101d64e9", |                 "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", | ||||||
|                 "sha256:1da0c98a85a6c6ed702d5556db3b09c91f9b0b78de37b7593e2de8d03238807a", |                 "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854", | ||||||
|                 "sha256:225bfff5d425c34e1fd562cef52d673579d59b967d9de06178850c4802af9039", |                 "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d", | ||||||
|                 "sha256:24f984fc7762ed5f806d9e8c4c77ea69fdb2afd987b4fd319ef06c87595a8c55", |                 "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278", | ||||||
|                 "sha256:25a7fd4de38f7ff99a37e18fa0098c3140286451bc823d1746ba80cec5b433a1", |                 "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a", | ||||||
|                 "sha256:2883b260f7a93235488699d39cbbd94fa7b175d3a8063fbfddd3e81ad9988cb2", |                 "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c", | ||||||
|                 "sha256:2a51ce96224eadd1845150b204389623c8e129fde5a67a84b972bd83a85c6c40", |                 "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f", | ||||||
|                 "sha256:2be0ad541bb9f059954ccf8877a49ed73877f862529575ff3d54bf4223e4dd61", |                 "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27", | ||||||
|                 "sha256:31a2cae5f059329f9cfe3d8d266d3da1543b60b60130d186d9b6a3c20a346361", |                 "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f", | ||||||
|                 "sha256:333c840a1303d1474f491e7be0b718226c730a39ead0f7dab2c7e6a2f3855555", |                 "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac", | ||||||
|                 "sha256:33d14369739c5d07e2e7102cdb0081a1fa46ed03215e07f097b34e020b83b1ae", |                 "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", | ||||||
|                 "sha256:35380671c3c921fe8adf31ad349dc6f7588b7e928dbe44e1093789734f607399", |                 "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97", | ||||||
|                 "sha256:359e7951f04ad35111b5ddce184db3391442345d0ab073aa63a95eb8af25a5ef", |                 "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", | ||||||
|                 "sha256:36aa167f69d8807ba7e341d67ea93e50fcaaf6bc433bb04939430fa3dab06f31", |                 "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919", | ||||||
|                 "sha256:395e3e1148fa7809016231f8065f30bb0dc285a97b4dc4360cd86e17bab58af7", |                 "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9", | ||||||
|                 "sha256:3e8d89c276234579cd3d095d5fa2a44eb10db9a218664a17b56363cddf226ff3", |                 "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4", | ||||||
|                 "sha256:3eb8849445c26b41c5a474061032c53e14fe92a11a5db969f722a2716cd12206", |                 "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c", | ||||||
|                 "sha256:3fd8bc2690e7c39eecdf9071b6a889ce7b22b72073863940edc2a0a23750ca90", |                 "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131", | ||||||
|                 "sha256:400bf470e4327e920883b51e255617dfe4496d4e80c3fea0b5a5d0bf2c404dd4", |                 "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5", | ||||||
|                 "sha256:4148dc9184ab79e356dc00a4199dc0ee8647973332cb385fc29a7cced49b9f9c", |                 "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd", | ||||||
|                 "sha256:433689845288f9a1ee5714444e65957be26d30915f7745091ede4a83cfb2d7bb", |                 "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089", | ||||||
|                 "sha256:43b61989068de9ce62296cde02beffabcadb65672207fc51e7af76dca75e6636", |                 "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", | ||||||
|                 "sha256:4523c4009c3f39d948e01962223c9f5538602e7087a628479b723c939fab262d", |                 "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6", | ||||||
|                 "sha256:483c2213a609e7db2c592bbc015da58b6c75af7360ca3c981f178110d9787bcf", |                 "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60", | ||||||
|                 "sha256:49633583eb7dc5cba61aaf7cdb2e9e662323ad394e543ee77af265736bcd3eaa", |                 "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", | ||||||
|                 "sha256:4b51f964fcbb02949fc546022e56cdb16cda457af485e9a3e8b78ac2ecf5d77e", |                 "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", | ||||||
|                 "sha256:4bf1340ae507f6da6360b24179c2083857c8ca7644aab65807023cf35404ea8d", |                 "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08", | ||||||
|                 "sha256:4fb49cfdb53af5041aba909be00cccfb2c0d0a2e09281bf542371c5fd36ad04c", |                 "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05", | ||||||
|                 "sha256:510b11e9c3b1a852876d1ccd8d5903684336d635214148637ceb27366c75a467", |                 "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2", | ||||||
|                 "sha256:513cb14c0cc31a4dfd849a4674b20c46d87b364f997bbcb02282306f5e187abf", |                 "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e", | ||||||
|                 "sha256:58560828ee0951bb125c6f2862fbc37f039996d19ceb6d8ff1905abf7da0bf3d", |                 "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c", | ||||||
|                 "sha256:58ab0d979c969983cdb97374698d847a4acffb217d543e172838864636ef10d9", |                 "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17", | ||||||
|                 "sha256:5982048129f40b082c2654de10c0f37c67a14f5ff9d37cf35be028ae982f26df", |                 "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62", | ||||||
|                 "sha256:5ab325fc86fbc077284c8d7f996d904d30e97904a87d6fb303dce6b3de7ebba9", |                 "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", | ||||||
|                 "sha256:5cc822ab90a70ea3a91e6aed3afac570b276b1278c6909b1d384f745bd09c714", |                 "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be", | ||||||
|                 "sha256:5f2b19b8d6fca432cb3acf48cf5243a7bf512988029b6e6fd27e9e8c0a204d85", |                 "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067", | ||||||
|                 "sha256:5fc72fbfebbf42c0856a824b8b0dc2b5cd2e4a896050281a21cfa6fed8879cb1", |                 "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02", | ||||||
|                 "sha256:6354e18a9be37bfa124d6b288a87fb30c673745806c92956f1a25e3ae6e76b96", |                 "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f", | ||||||
|                 "sha256:678f66462058dd978702db17eb6a3633d634f7aa0deaea61e0a674152766d3fc", |                 "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", | ||||||
|                 "sha256:68950bc08f9735306322bfc16a18391fcaac99ded2509e1cc41d03ccb6013cfe", |                 "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840", | ||||||
|                 "sha256:68ef5377eb582fa4343c9d0b57a5b094046d447b4c73dd9fbd9ffb216f829e7d", |                 "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5", | ||||||
|                 "sha256:6b4c19525c3538fbc0bbda6229f9682fb8199ce9ac37395880e6952798e00373", |                 "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807", | ||||||
|                 "sha256:6bb69bf3b6500f195c3deb69c1205ba8fc3cb21d1915f1f158a10d6b1ef29b6a", |                 "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", | ||||||
|                 "sha256:6e19401742ed7b69e51d8e4df3c03ad5ec65a83b36244479fd70edde2828a5d9", |                 "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", | ||||||
|                 "sha256:6f4a53af9e81d757756508b57cae1cf28293f0f31b9fa2bfcb416cc7fb230f9d", |                 "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864", | ||||||
|                 "sha256:6fda87808429c520a002a85d6e7cdadbf58231d60e96260976c5b8f9a12a8e13", |                 "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e", | ||||||
|                 "sha256:78f841523729e43e3928a364ec46e2e3f80e6625a4f62aca5c345f3f626c6e8a", |                 "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a", | ||||||
|                 "sha256:7a6ebfac28fd51890a61df36ef202adbd77d00ee5aca4a3dadb3d9ed49cfb929", |                 "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35", | ||||||
|                 "sha256:7b0202ebf2268954090209a84f9897345719e46a57c5f2c9b7b250ca0a9d3e63", |                 "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", | ||||||
|                 "sha256:8117839a9bdbba86e7f9df57018fe3b96cec934c3940b591b0fd3fbfb485864a", |                 "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a", | ||||||
|                 "sha256:82e1ad4ca170e8af4c928b67cff731b6296e6a0a0981b97b2eb7c275cc4e15bd", |                 "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3", | ||||||
|                 "sha256:836a4bfe0cc6d36dc9a9cc1a7b391265bf6ce9d1eb1eac62ac5139f5d8d9a6fa", |                 "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52", | ||||||
|                 "sha256:84af1cf7bfdcbc6fcf5a5f70cc9896205e0350306e4dd73d54b6a18894f79386", |                 "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", | ||||||
|                 "sha256:84e35afd9e10b2698e6f2f32256678cb23ca6c1568d02628033a837638b3ed12", |                 "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31", | ||||||
|                 "sha256:884f1806609c2c66564082540cffc96868c5571c7c3cf3a783f63f2fb49bd3cd", |                 "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89", | ||||||
|                 "sha256:8a150392102c402c538190730fda06f3bce654fc498865579a9f2c1d2b425833", |                 "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de", | ||||||
|                 "sha256:8e21d927469d04b39386255bf00d0feedead16f6253dcc85e9e10ddebc334084", |                 "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6", | ||||||
|                 "sha256:8e96ca781e0c01e32115912ebdf7b3fb0780ce748b80d7d28a0802fa9fbaf44e", |                 "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36", | ||||||
|                 "sha256:8ee4c2a75af9fe21269a4a0898c5425afb01af1f5d276063f57e2ae1bc64e191", |                 "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c", | ||||||
|                 "sha256:91bc66f878557313c2a6bcf396e7befcffe5ab4354cfe4427318968af31143c3", |                 "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154", | ||||||
|                 "sha256:951e71da6c89d354572098bada5ba5b5dc3a9390c933af8a614e37755d3d1840", |                 "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb", | ||||||
|                 "sha256:99b2863c1365f43f74199c980a3d40f18a218fbe683dd64e470199db426c4d6a", |                 "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", | ||||||
|                 "sha256:9a8fbf506fde1529a1e3698198fe64bfbe2e0c09557bc6a7dcf872e7c01fec40", |                 "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd", | ||||||
|                 "sha256:9ce048deb1e033e7a865ca384770bccc11d44179cf09e5193a535c4c2f497bdc", |                 "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3", | ||||||
|                 "sha256:9fe94d9d2a2b4edd7a4b22adcd45814b1b59b03feb00e56deb2e89747aec7bfe", |                 "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f", | ||||||
|                 "sha256:a291d0b4243a259c8ea7e2b84eb9ccb76370e569298875a7c5e3e71baf49057a", |                 "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78", | ||||||
|                 "sha256:a5c022bb0d453192426221605efc865373dde43b17822a264671c53b068ac20c", |                 "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", | ||||||
|                 "sha256:abb4785894936d7682635726613c44578c420a096729f1978cd061a7e72d5275", |                 "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618", | ||||||
|                 "sha256:b872c86d8d71827235c7077461c502feb2db3f87d9d6d5a9daa64287d75e4fa0", |                 "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", | ||||||
|                 "sha256:bf37b72834e7239cf84d4a0b2c050e7f9e48bced97bad9bdf98d26b8eb72e846", |                 "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4", | ||||||
|                 "sha256:c0c431e4be5c1a0c6654e0c31c661cd89e0ca956ef65305c3c3fd96f4e72ca39", |                 "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c", | ||||||
|                 "sha256:c5726eec789ee38f2c53b10b1821457b82274f81f4f746bb1e666d8741fcfadb", |                 "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c", | ||||||
|                 "sha256:c6fcb3fa3855d583aa57b94cf146f7781d5d5bc06cb95cb3afece33d31aac39b", |                 "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330", | ||||||
|                 "sha256:c86679f443e7085ea55a7376462553996c688395d18ef3f0d3dbad7838f857a2", |                 "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8", | ||||||
|                 "sha256:c91e3c04f5191fd3fb68764bddeaf02025492d5d9f23343b283870f6ace69708", |                 "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792", | ||||||
|                 "sha256:c921ad596ff1a82f9c692b0758c944355abc9f0de97a4c13ca60ffc6d8dc15d4", |                 "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025", | ||||||
|                 "sha256:c9ed88b398ba7e3bad7bd64d66cc01dcde9cfcb7ec629a6fd78a82fa0b559d78", |                 "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9", | ||||||
|                 "sha256:cd2ac6b919f7fed71b17fe0b4603c092a4c9b5bae414817c9c81d3c22d1e1bcc", |                 "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f", | ||||||
|                 "sha256:d28ca7066d6cdd347a50d8b725dc10d9a1d6a1cce09836cf071ea6a2d4908be0", |                 "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01", | ||||||
|                 "sha256:d29e235ce13c91902ef3efc3d883a677655b3908b1cbc73dee816e5e1f8f7739", |                 "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", | ||||||
|                 "sha256:d8b5ee4ae9170e2775d495b81f414cc20268041c42571530513496ba61e94ba3", |                 "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4", | ||||||
|                 "sha256:db72e40628967f6dc572020d04b5f800d71264e0531c6da35097e73bdf38b003", |                 "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f", | ||||||
|                 "sha256:df45c4073bed486ea2f18757057953afed8dd77add7276ff01bccb79982cf46c", |                 "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd", | ||||||
|                 "sha256:dfa5f5c0a4c8fced1422dc2ca7eefd872d5d13eb33cf324361dbf1dbfba0a9fe", |                 "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", | ||||||
|                 "sha256:e015833384ca3e1a0565a79f5d953b0629d9138021c27ad37c92a9fa1af7623c", |                 "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab", | ||||||
|                 "sha256:e15315691fe2253eb447503153acef4d7223dfe7e7702f9ed66539fcd0c43801", |                 "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc", | ||||||
|                 "sha256:e65466b31be1070b4a5b7dbfbd14b247884cb8e8b79c64fb0f36b472912dbaea", |                 "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676", | ||||||
|                 "sha256:e7820bb0d65e3ce1e3e70b6708c2f66143f55912fa02f4b618d0f08b61575f12", |                 "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381", | ||||||
|                 "sha256:e851a051f7260e6d688267eb039c81f05f23a19431bd7dfa4bf5e3cb34c108cd", |                 "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed", | ||||||
|                 "sha256:e9f9feee7f334b72ceae46313333d002b56f325b5f04271b4ae2aadd9e993ae4", |                 "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb", | ||||||
|                 "sha256:eb40f828bc2f73f777d1eb8fee2e86cd9692a4518b63b6b5aa8af915dfd3207b", |                 "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967", | ||||||
|                 "sha256:eb704155e73b833801c247f39d562229c0303f54770ca14fb1c053acb376cf10", |                 "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", | ||||||
|                 "sha256:edb1bfd45227dec8d50bc7c7d86463cd8728bcc574f9b07de7369880de4626a3", |                 "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae", | ||||||
|                 "sha256:ee7d9d5537daf6d5c74a83b38a638cc001b648096c1cae8ef695b0c919d9d379", |                 "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c", | ||||||
|                 "sha256:f57783fbaf648205ac50ae7d646f27582fc706be3977e87c3c124e7a92407b10", |                 "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206", | ||||||
|                 "sha256:ff63a92f6e249514ef35bc795de10745be0226eaea06eb48b4bbeaa0c8850a4a" |                 "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.8'", |             "markers": "python_version >= '3.8'", | ||||||
|             "version": "==2.27.0" |             "version": "==2.27.1" | ||||||
|         }, |         }, | ||||||
|         "pygments": { |         "pygments": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -1083,6 +1092,57 @@ | |||||||
|             "markers": "python_version >= '3.9'", |             "markers": "python_version >= '3.9'", | ||||||
|             "version": "==3.2.0" |             "version": "==3.2.0" | ||||||
|         }, |         }, | ||||||
|  |         "pyqt6": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:0adb7914c732ad1dee46d9cec838a98cb2b11bc38cc3b7b36fbd8701ae64bf47", | ||||||
|  |                 "sha256:2d771fa0981514cb1ee937633dfa64f14caa902707d9afffab66677f3a73e3da", | ||||||
|  |                 "sha256:3672a82ccd3a62e99ab200a13903421e2928e399fda25ced98d140313ad59cb9", | ||||||
|  |                 "sha256:7f397f4b38b23b5588eb2c0933510deb953d96b1f0323a916c4839c2a66ccccc", | ||||||
|  |                 "sha256:c2f202b7941aa74e5c7e1463a6f27d9131dbc1e6cabe85571d7364f5b3de7397", | ||||||
|  |                 "sha256:f053378e3aef6248fa612c8afddda17f942fb63f9fe8a9aeb2a6b6b4cbb0eba9", | ||||||
|  |                 "sha256:fa3954698233fe286a8afc477b84d8517f0788eb46b74da69d3ccc0170d3714c" | ||||||
|  |             ], | ||||||
|  |             "index": "pypi", | ||||||
|  |             "markers": "python_version >= '3.8'", | ||||||
|  |             "version": "==6.7.1" | ||||||
|  |         }, | ||||||
|  |         "pyqt6-qt6": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:36ea0892b8caeb983af3f285f45fb8dfbb93cfd972439f4e01b7efb2868f6230", | ||||||
|  |                 "sha256:50c7482bcdcf2bb78af257fb10ed8b582f8daf91d829782393bc50ac5a0a900c", | ||||||
|  |                 "sha256:8551732984fb36a5f4f3db51eafc4e8e6caf18617365830285306f2db17a94c2", | ||||||
|  |                 "sha256:cb525fdd393332de60887953029276a44de480fce1d785251ae639580f5e7246", | ||||||
|  |                 "sha256:f517a93b6b1a814d4aa6587adc312e812ebaf4d70415bb15cfb44268c5ad3f5f" | ||||||
|  |             ], | ||||||
|  |             "version": "==6.7.3" | ||||||
|  |         }, | ||||||
|  |         "pyqt6-sip": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:056af69d1d8d28d5968066ec5da908afd82fc0be07b67cf2b84b9f02228416ce", | ||||||
|  |                 "sha256:08dd81037a2864982ece2bf9891f3bf4558e247034e112993ea1a3fe239458cb", | ||||||
|  |                 "sha256:2559afa68825d08de09d71c42f3b6ad839dcc30f91e7c6d0785e07830d5541a5", | ||||||
|  |                 "sha256:2f74cf3d6d9cab5152bd9f49d570b2dfb87553ebb5c4919abfde27f5b9fd69d4", | ||||||
|  |                 "sha256:33d9b399fc9c9dc99496266842b0fb2735d924604774e97cf9b555667cc0fc59", | ||||||
|  |                 "sha256:6bce6bc5870d9e87efe5338b1ee4a7b9d7d26cdd16a79a5757d80b6f25e71edc", | ||||||
|  |                 "sha256:755beb5d271d081e56618fb30342cdd901464f721450495cb7cb0212764da89e", | ||||||
|  |                 "sha256:7a0bbc0918eab5b6351735d40cf22cbfa5aa2476b55e0d5fe881aeed7d871c29", | ||||||
|  |                 "sha256:7f84c472afdc7d316ff683f63129350d645ef82d9b3fd75a609b08472d1f7291", | ||||||
|  |                 "sha256:835ed22eab977f75fd77e60d4ff308a1fa794b1d0c04849311f36d2a080cdf3b", | ||||||
|  |                 "sha256:9ea9223c94906efd68148f12ae45b51a21d67e86704225ddc92bce9c54e4d93c", | ||||||
|  |                 "sha256:a5c086b7c9c7996ea9b7522646cc24eebbf3591ec9dd38f65c0a3fdb0dbeaac7", | ||||||
|  |                 "sha256:b1bf29e95f10a8a00819dac804ca7e5eba5fc1769adcd74c837c11477bf81954", | ||||||
|  |                 "sha256:b203b6fbae4a8f2d27f35b7df46200057033d9ecd9134bcf30e3eab66d43572c", | ||||||
|  |                 "sha256:beaddc1ec96b342f4e239702f91802706a80cb403166c2da318cec4ad8b790cb", | ||||||
|  |                 "sha256:cd81144b0770084e8005d3a121c9382e6f9bc8d0bb320dd618718ffe5090e0e6", | ||||||
|  |                 "sha256:cedd554c643e54c4c2e12b5874781a87441a1b405acf3650a4a2e1df42aae231", | ||||||
|  |                 "sha256:d8b22a6850917c68ce83fc152a8b606ecb2efaaeed35be53110468885d6cdd9d", | ||||||
|  |                 "sha256:dd168667addf01f8a4b0fa7755323e43e4cd12ca4bade558c61f713a5d48ba1a", | ||||||
|  |                 "sha256:f57275b5af774529f9838adcfb58869ba3ebdaf805daea113bb0697a96a3f3cb", | ||||||
|  |                 "sha256:fbb249b82c53180f1420571ece5dc24fea1188ba435923edd055599dffe7abfb" | ||||||
|  |             ], | ||||||
|  |             "markers": "python_version >= '3.8'", | ||||||
|  |             "version": "==13.8.0" | ||||||
|  |         }, | ||||||
|         "python-dateutil": { |         "python-dateutil": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", |                 "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", | ||||||
| @@ -1188,6 +1248,39 @@ | |||||||
|             "markers": "python_full_version >= '3.8.0'", |             "markers": "python_full_version >= '3.8.0'", | ||||||
|             "version": "==13.9.4" |             "version": "==13.9.4" | ||||||
|         }, |         }, | ||||||
|  |         "scikit-learn": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:03b6158efa3faaf1feea3faa884c840ebd61b6484167c711548fce208ea09445", | ||||||
|  |                 "sha256:178ddd0a5cb0044464fc1bfc4cca5b1833bfc7bb022d70b05db8530da4bb3dd3", | ||||||
|  |                 "sha256:1ff45e26928d3b4eb767a8f14a9a6efbf1cbff7c05d1fb0f95f211a89fd4f5de", | ||||||
|  |                 "sha256:299406827fb9a4f862626d0fe6c122f5f87f8910b86fe5daa4c32dcd742139b6", | ||||||
|  |                 "sha256:2d4cad1119c77930b235579ad0dc25e65c917e756fe80cab96aa3b9428bd3fb0", | ||||||
|  |                 "sha256:394397841449853c2290a32050382edaec3da89e35b3e03d6cc966aebc6a8ae6", | ||||||
|  |                 "sha256:3a686885a4b3818d9e62904d91b57fa757fc2bed3e465c8b177be652f4dd37c8", | ||||||
|  |                 "sha256:3b923d119d65b7bd555c73be5423bf06c0105678ce7e1f558cb4b40b0a5502b1", | ||||||
|  |                 "sha256:3bed4909ba187aca80580fe2ef370d9180dcf18e621a27c4cf2ef10d279a7efe", | ||||||
|  |                 "sha256:52788f48b5d8bca5c0736c175fa6bdaab2ef00a8f536cda698db61bd89c551c1", | ||||||
|  |                 "sha256:57cc1786cfd6bd118220a92ede80270132aa353647684efa385a74244a41e3b1", | ||||||
|  |                 "sha256:643964678f4b5fbdc95cbf8aec638acc7aa70f5f79ee2cdad1eec3df4ba6ead8", | ||||||
|  |                 "sha256:6c16d84a0d45e4894832b3c4d0bf73050939e21b99b01b6fd59cbb0cf39163b6", | ||||||
|  |                 "sha256:757c7d514ddb00ae249832fe87100d9c73c6ea91423802872d9e74970a0e40b9", | ||||||
|  |                 "sha256:8c412ccc2ad9bf3755915e3908e677b367ebc8d010acbb3f182814524f2e5540", | ||||||
|  |                 "sha256:b0768ad641981f5d3a198430a1d31c3e044ed2e8a6f22166b4d546a5116d7908", | ||||||
|  |                 "sha256:b4237ed7b3fdd0a4882792e68ef2545d5baa50aca3bb45aa7df468138ad8f94d", | ||||||
|  |                 "sha256:b7b0f9a0b1040830d38c39b91b3a44e1b643f4b36e36567b80b7c6bd2202a27f", | ||||||
|  |                 "sha256:c15b1ca23d7c5f33cc2cb0a0d6aaacf893792271cddff0edbd6a40e8319bc113", | ||||||
|  |                 "sha256:ca64b3089a6d9b9363cd3546f8978229dcbb737aceb2c12144ee3f70f95684b7", | ||||||
|  |                 "sha256:e9a702e2de732bbb20d3bad29ebd77fc05a6b427dc49964300340e4c9328b3f5", | ||||||
|  |                 "sha256:f60021ec1574e56632be2a36b946f8143bf4e5e6af4a06d85281adc22938e0dd", | ||||||
|  |                 "sha256:f7284ade780084d94505632241bf78c44ab3b6f1e8ccab3d2af58e0e950f9c12", | ||||||
|  |                 "sha256:f763897fe92d0e903aa4847b0aec0e68cadfff77e8a0687cabd946c89d17e675", | ||||||
|  |                 "sha256:f8b0ccd4a902836493e026c03256e8b206656f91fbcc4fde28c57a5b752561f1", | ||||||
|  |                 "sha256:f932a02c3f4956dfb981391ab24bda1dbd90fe3d628e4b42caef3e041c67707a" | ||||||
|  |             ], | ||||||
|  |             "index": "pypi", | ||||||
|  |             "markers": "python_version >= '3.9'", | ||||||
|  |             "version": "==1.5.2" | ||||||
|  |         }, | ||||||
|         "scipy": { |         "scipy": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0c2f95de3b04e26f5f3ad5bb05e74ba7f68b837133a4492414b3afd79dfe540e", |                 "sha256:0c2f95de3b04e26f5f3ad5bb05e74ba7f68b837133a4492414b3afd79dfe540e", | ||||||
| @@ -1317,13 +1410,21 @@ | |||||||
|             "markers": "python_version >= '3.8'", |             "markers": "python_version >= '3.8'", | ||||||
|             "version": "==0.41.3" |             "version": "==0.41.3" | ||||||
|         }, |         }, | ||||||
|  |         "threadpoolctl": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107", | ||||||
|  |                 "sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467" | ||||||
|  |             ], | ||||||
|  |             "markers": "python_version >= '3.8'", | ||||||
|  |             "version": "==3.5.0" | ||||||
|  |         }, | ||||||
|         "typer": { |         "typer": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:5b59580fd925e89463a29d363e0a43245ec02765bde9fb77d39e5d0f29dd7157", |                 "sha256:af58f737f8d0c0c37b9f955a6d39000b9ff97813afcbeef56af5e37cf743b45a", | ||||||
|                 "sha256:9d444cb96cc268ce6f8b94e13b4335084cef4c079998a9f4851a90229a3bd25c" |                 "sha256:f476233a25770ab3e7b2eebf7c68f3bc702031681a008b20167573a4b7018f09" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.7'", |             "markers": "python_version >= '3.7'", | ||||||
|             "version": "==0.13.1" |             "version": "==0.14.0" | ||||||
|         }, |         }, | ||||||
|         "typing-extensions": { |         "typing-extensions": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -1488,91 +1589,79 @@ | |||||||
|         }, |         }, | ||||||
|         "watchfiles": { |         "watchfiles": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:01550ccf1d0aed6ea375ef259706af76ad009ef5b0203a3a4cce0f6024f9b68a", |                 "sha256:06d828fe2adc4ac8a64b875ca908b892a3603d596d43e18f7948f3fef5fc671c", | ||||||
|                 "sha256:01def80eb62bd5db99a798d5e1f5f940ca0a05986dcfae21d833af7a46f7ee22", |                 "sha256:074c7618cd6c807dc4eaa0982b4a9d3f8051cd0b72793511848fd64630174b17", | ||||||
|                 "sha256:07cdef0c84c03375f4e24642ef8d8178e533596b229d32d2bbd69e5128ede02a", |                 "sha256:09551237645d6bff3972592f2aa5424df9290e7a2e15d63c5f47c48cde585935", | ||||||
|                 "sha256:083dc77dbdeef09fa44bb0f4d1df571d2e12d8a8f985dccde71ac3ac9ac067a0", |                 "sha256:0fc3bf0effa2d8075b70badfdd7fb839d7aa9cea650d17886982840d71fdeabf", | ||||||
|                 "sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827", |                 "sha256:12ab123135b2f42517f04e720526d41448667ae8249e651385afb5cda31fedc0", | ||||||
|                 "sha256:21ab23fdc1208086d99ad3f69c231ba265628014d4aed31d4e8746bd59e88cd1", |                 "sha256:13a4f9ee0cd25682679eea5c14fc629e2eaa79aab74d963bc4e21f43b8ea1877", | ||||||
|                 "sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c", |                 "sha256:1d19df28f99d6a81730658fbeb3ade8565ff687f95acb59665f11502b441be5f", | ||||||
|                 "sha256:2e28d91ef48eab0afb939fa446d8ebe77e2f7593f5f463fd2bb2b14132f95b6e", |                 "sha256:1e176b6b4119b3f369b2b4e003d53a226295ee862c0962e3afd5a1c15680b4e3", | ||||||
|                 "sha256:2efec17819b0046dde35d13fb8ac7a3ad877af41ae4640f4109d9154ed30a188", |                 "sha256:1ee5edc939f53466b329bbf2e58333a5461e6c7b50c980fa6117439e2c18b42d", | ||||||
|                 "sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b", |                 "sha256:1f73c2147a453315d672c1ad907abe6d40324e34a185b51e15624bc793f93cc6", | ||||||
|                 "sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5", |                 "sha256:1ff236d7a3f4b0a42f699a22fc374ba526bc55048a70cbb299661158e1bb5e1f", | ||||||
|                 "sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90", |                 "sha256:245fab124b9faf58430da547512d91734858df13f2ddd48ecfa5e493455ffccb", | ||||||
|                 "sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef", |                 "sha256:28babb38cf2da8e170b706c4b84aa7e4528a6fa4f3ee55d7a0866456a1662041", | ||||||
|                 "sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b", |                 "sha256:28fb64b5843d94e2c2483f7b024a1280662a44409bedee8f2f51439767e2d107", | ||||||
|                 "sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15", |                 "sha256:29cf884ad4285d23453c702ed03d689f9c0e865e3c85d20846d800d4787de00f", | ||||||
|                 "sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48", |                 "sha256:2a825ba4b32c214e3855b536eb1a1f7b006511d8e64b8215aac06eb680642d84", | ||||||
|                 "sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e", |                 "sha256:2ac778a460ea22d63c7e6fb0bc0f5b16780ff0b128f7f06e57aaec63bd339285", | ||||||
|                 "sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df", |                 "sha256:2c2696611182c85eb0e755b62b456f48debff484b7306b56f05478b843ca8ece", | ||||||
|                 "sha256:449f43f49c8ddca87c6b3980c9284cab6bd1f5c9d9a2b00012adaaccd5e7decd", |                 "sha256:2d9c0518fabf4a3f373b0a94bb9e4ea7a1df18dec45e26a4d182aa8918dee855", | ||||||
|                 "sha256:4933a508d2f78099162da473841c652ad0de892719043d3f07cc83b33dfd9d91", |                 "sha256:2de52b499e1ab037f1a87cb8ebcb04a819bf087b1015a4cf6dcf8af3c2a2613e", | ||||||
|                 "sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d", |                 "sha256:37566c844c9ce3b5deb964fe1a23378e575e74b114618d211fbda8f59d7b5dab", | ||||||
|                 "sha256:49fb58bcaa343fedc6a9e91f90195b20ccb3135447dc9e4e2570c3a39565853e", |                 "sha256:3d94fd83ed54266d789f287472269c0def9120a2022674990bd24ad989ebd7a0", | ||||||
|                 "sha256:4a7fa2bc0efef3e209a8199fd111b8969fe9db9c711acc46636686331eda7dd4", |                 "sha256:48051d1c504448b2fcda71c5e6e3610ae45de6a0b8f5a43b961f250be4bdf5a8", | ||||||
|                 "sha256:4abf4ad269856618f82dee296ac66b0cd1d71450fc3c98532d93798e73399b7a", |                 "sha256:487d15927f1b0bd24e7df921913399bb1ab94424c386bea8b267754d698f8f0e", | ||||||
|                 "sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370", |                 "sha256:4a3b33c3aefe9067ebd87846806cd5fc0b017ab70d628aaff077ab9abf4d06b3", | ||||||
|                 "sha256:4d28cea3c976499475f5b7a2fec6b3a36208656963c1a856d328aeae056fc5c1", |                 "sha256:4ff9c7e84e8b644a8f985c42bcc81457240316f900fc72769aaedec9d088055a", | ||||||
|                 "sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea", |                 "sha256:533a7cbfe700e09780bb31c06189e39c65f06c7f447326fee707fd02f9a6e945", | ||||||
|                 "sha256:54ca90a9ae6597ae6dc00e7ed0a040ef723f84ec517d3e7ce13e63e4bc82fa04", |                 "sha256:53ae447f06f8f29f5ab40140f19abdab822387a7c426a369eb42184b021e97eb", | ||||||
|                 "sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896", |                 "sha256:550109001920a993a4383b57229c717fa73627d2a4e8fcb7ed33c7f1cddb0c85", | ||||||
|                 "sha256:5c51749f3e4e269231510da426ce4a44beb98db2dce9097225c338f815b05d4f", |                 "sha256:5bbd0311588c2de7f9ea5cf3922ccacfd0ec0c1922870a2be503cc7df1ca8be7", | ||||||
|                 "sha256:632676574429bee8c26be8af52af20e0c718cc7f5f67f3fb658c71928ccd4f7f", |                 "sha256:5dccfc70480087567720e4e36ec381bba1ed68d7e5f368fe40c93b3b1eba0105", | ||||||
|                 "sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43", |                 "sha256:5f75cd42e7e2254117cf37ff0e68c5b3f36c14543756b2da621408349bd9ca7c", | ||||||
|                 "sha256:6bdcfa3cd6fdbdd1a068a52820f46a815401cbc2cb187dd006cb076675e7b735", |                 "sha256:648e2b6db53eca6ef31245805cd528a16f56fa4cc15aeec97795eaf713c11435", | ||||||
|                 "sha256:7138eff8baa883aeaa074359daabb8b6c1e73ffe69d5accdc907d62e50b1c0da", |                 "sha256:774ef36b16b7198669ce655d4f75b4c3d370e7f1cbdfb997fb10ee98717e2058", | ||||||
|                 "sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a", |                 "sha256:8a2127cd68950787ee36753e6d401c8ea368f73beaeb8e54df5516a06d1ecd82", | ||||||
|                 "sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61", |                 "sha256:90004553be36427c3d06ec75b804233f8f816374165d5225b93abd94ba6e7234", | ||||||
|                 "sha256:78470906a6be5199524641f538bd2c56bb809cd4bf29a566a75051610bc982c3", |                 "sha256:905f69aad276639eff3893759a07d44ea99560e67a1cf46ff389cd62f88872a2", | ||||||
|                 "sha256:7ae3e208b31be8ce7f4c2c0034f33406dd24fbce3467f77223d10cd86778471c", |                 "sha256:9122b8fdadc5b341315d255ab51d04893f417df4e6c1743b0aac8bf34e96e025", | ||||||
|                 "sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f", |                 "sha256:9272fdbc0e9870dac3b505bce1466d386b4d8d6d2bacf405e603108d50446940", | ||||||
|                 "sha256:82ae557a8c037c42a6ef26c494d0631cacca040934b101d001100ed93d43f361", |                 "sha256:936f362e7ff28311b16f0b97ec51e8f2cc451763a3264640c6ed40fb252d1ee4", | ||||||
|                 "sha256:82b2509f08761f29a0fdad35f7e1638b8ab1adfa2666d41b794090361fb8b855", |                 "sha256:947ccba18a38b85c366dafeac8df2f6176342d5992ca240a9d62588b214d731f", | ||||||
|                 "sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327", |                 "sha256:95dc785bc284552d044e561b8f4fe26d01ab5ca40d35852a6572d542adfeb4bc", | ||||||
|                 "sha256:85d5f0c7771dcc7a26c7a27145059b6bb0ce06e4e751ed76cdf123d7039b60b5", |                 "sha256:95de85c254f7fe8cbdf104731f7f87f7f73ae229493bebca3722583160e6b152", | ||||||
|                 "sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab", |                 "sha256:9b4fb98100267e6a5ebaff6aaa5d20aea20240584647470be39fe4823012ac96", | ||||||
|                 "sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633", |                 "sha256:9c01446626574561756067f00b37e6b09c8622b0fc1e9fdbc7cbcea328d4e514", | ||||||
|                 "sha256:951088d12d339690a92cef2ec5d3cfd957692834c72ffd570ea76a6790222777", |                 "sha256:9c9a8d8fd97defe935ef8dd53d562e68942ad65067cd1c54d6ed8a088b1d931d", | ||||||
|                 "sha256:95cf3b95ea665ab03f5a54765fa41abf0529dbaf372c3b83d91ad2cfa695779b", |                 "sha256:9e1d9284cc84de7855fcf83472e51d32daf6f6cecd094160192628bc3fee1b78", | ||||||
|                 "sha256:96619302d4374de5e2345b2b622dc481257a99431277662c30f606f3e22f42be", |                 "sha256:a0abf173975eb9dd17bb14c191ee79999e650997cc644562f91df06060610e62", | ||||||
|                 "sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f", |                 "sha256:a2218e78e2c6c07b1634a550095ac2a429026b2d5cbcd49a594f893f2bb8c936", | ||||||
|                 "sha256:9a60e2bf9dc6afe7f743e7c9b149d1fdd6dbf35153c78fe3a14ae1a9aee3d98b", |                 "sha256:a5a7a06cfc65e34fd0a765a7623c5ba14707a0870703888e51d3d67107589817", | ||||||
|                 "sha256:9f895d785eb6164678ff4bb5cc60c5996b3ee6df3edb28dcdeba86a13ea0465e", |                 "sha256:b2bca898c1dc073912d3db7fa6926cc08be9575add9e84872de2c99c688bac4e", | ||||||
|                 "sha256:a2a9891723a735d3e2540651184be6fd5b96880c08ffe1a98bae5017e65b544b", |                 "sha256:b46e15c34d4e401e976d6949ad3a74d244600d5c4b88c827a3fdf18691a46359", | ||||||
|                 "sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366", |                 "sha256:b551c465a59596f3d08170bd7e1c532c7260dd90ed8135778038e13c5d48aa81", | ||||||
|                 "sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823", |                 "sha256:b555a93c15bd2c71081922be746291d776d47521a00703163e5fbe6d2a402399", | ||||||
|                 "sha256:acbfa31e315a8f14fe33e3542cbcafc55703b8f5dcbb7c1eecd30f141df50db3", |                 "sha256:bc338ce9f8846543d428260fa0f9a716626963148edc937d71055d01d81e1525", | ||||||
|                 "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1", |                 "sha256:bedf84835069f51c7b026b3ca04e2e747ea8ed0a77c72006172c72d28c9f69fc", | ||||||
|                 "sha256:b3ef2c69c655db63deb96b3c3e587084612f9b1fa983df5e0c3379d41307467f", |                 "sha256:c3d258d78341d5d54c0c804a5b7faa66cd30ba50b2756a7161db07ce15363b8d", | ||||||
|                 "sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418", |                 "sha256:c83a6d33a9eda0af6a7470240d1af487807adc269704fe76a4972dd982d16236", | ||||||
|                 "sha256:b665caeeda58625c3946ad7308fbd88a086ee51ccb706307e5b1fa91556ac886", |                 "sha256:c9a13ac46b545a7d0d50f7641eefe47d1597e7d1783a5d89e09d080e6dff44b0", | ||||||
|                 "sha256:b74fdffce9dfcf2dc296dec8743e5b0332d15df19ae464f0e249aa871fc1c571", |                 "sha256:cf517701a4a872417f4e02a136e929537743461f9ec6cdb8184d9a04f4843545", | ||||||
|                 "sha256:b995bfa6bf01a9e09b884077a6d37070464b529d8682d7691c2d3b540d357a0c", |                 "sha256:d2b39aa8edd9e5f56f99a2a2740a251dc58515398e9ed5a4b3e5ff2827060755", | ||||||
|                 "sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94", |                 "sha256:d3572d4c34c4e9c33d25b3da47d9570d5122f8433b9ac6519dca49c2740d23cd", | ||||||
|                 "sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428", |                 "sha256:d562a6114ddafb09c33246c6ace7effa71ca4b6a2324a47f4b09b6445ea78941", | ||||||
|                 "sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234", |                 "sha256:e1ed613ee107269f66c2df631ec0fc8efddacface85314d392a4131abe299f00", | ||||||
|                 "sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6", |                 "sha256:e3750434c83b61abb3163b49c64b04180b85b4dabb29a294513faec57f2ffdb7", | ||||||
|                 "sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968", |                 "sha256:eba98901a2eab909dbd79681190b9049acc650f6111fde1845484a4450761e98", | ||||||
|                 "sha256:d337193bbf3e45171c8025e291530fb7548a93c45253897cd764a6a71c937ed9", |                 "sha256:f159ac795785cde4899e0afa539f4c723fb5dd336ce5605bc909d34edd00b79b", | ||||||
|                 "sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c", |                 "sha256:f8c4f3a1210ed099a99e6a710df4ff2f8069411059ffe30fa5f9467ebed1256b", | ||||||
|                 "sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e", |                 "sha256:fa13d604fcb9417ae5f2e3de676e66aa97427d888e83662ad205bed35a313176", | ||||||
|                 "sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab", |                 "sha256:fbd0ab7a9943bbddb87cbc2bf2f09317e74c77dc55b1f5657f81d04666c25269", | ||||||
|                 "sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec", |                 "sha256:ffd98a299b0a74d1b704ef0ed959efb753e656a4e0425c14e46ae4c3cbdd2919" | ||||||
|                 "sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444", |  | ||||||
|                 "sha256:e5171ef898299c657685306d8e1478a45e9303ddcd8ac5fed5bd52ad4ae0b69b", |  | ||||||
|                 "sha256:e94e98c7cb94cfa6e071d401ea3342767f28eb5a06a58fafdc0d2a4974f4f35c", |  | ||||||
|                 "sha256:ec39698c45b11d9694a1b635a70946a5bad066b593af863460a8e600f0dff1ca", |  | ||||||
|                 "sha256:ed9aba6e01ff6f2e8285e5aa4154e2970068fe0fc0998c4380d0e6278222269b", |  | ||||||
|                 "sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18", |  | ||||||
|                 "sha256:ee82c98bed9d97cd2f53bdb035e619309a098ea53ce525833e26b93f673bc318", |  | ||||||
|                 "sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07", |  | ||||||
|                 "sha256:f7d9b87c4c55e3ea8881dfcbf6d61ea6775fffed1fedffaa60bd047d3c08c430", |  | ||||||
|                 "sha256:f83df90191d67af5a831da3a33dd7628b02a95450e168785586ed51e6d28943c", |  | ||||||
|                 "sha256:fca9433a45f18b7c779d2bae7beeec4f740d28b788b117a48368d95a3233ed83", |  | ||||||
|                 "sha256:fd92bbaa2ecdb7864b7600dcdb6f2f1db6e0346ed425fbd01085be04c63f0b05" |  | ||||||
|             ], |             ], | ||||||
|             "version": "==0.24.0" |             "version": "==1.0.0" | ||||||
|         }, |         }, | ||||||
|         "websockets": { |         "websockets": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -1650,11 +1739,11 @@ | |||||||
|         }, |         }, | ||||||
|         "xarray": { |         "xarray": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:ae1d38cb44a0324dfb61e492394158ae22389bf7de9f3c174309c17376df63a0", |                 "sha256:1ccace44573ddb862e210ad3ec204210654d2c750bec11bbe7d842dfc298591f", | ||||||
|                 "sha256:e369e2bac430e418c2448e5b96f07da4635f98c1319aa23cfeb3fbcb9a01d2e0" |                 "sha256:6ee94f63ddcbdd0cf3909d1177f78cdac756640279c0e32ae36819a89cdaba37" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.10'", |             "markers": "python_version >= '3.10'", | ||||||
|             "version": "==2024.10.0" |             "version": "==2024.11.0" | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|     "develop": { |     "develop": { | ||||||
| @@ -1718,12 +1807,12 @@ | |||||||
|         }, |         }, | ||||||
|         "httpx": { |         "httpx": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", |                 "sha256:0858d3bab51ba7e386637f22a61d8ccddaeec5f3fe4209da3a6168dbb91573e0", | ||||||
|                 "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2" |                 "sha256:dc0b419a0cfeb6e8b34e85167c0da2671206f5095f1baa9663d23bcfd6b535fc" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "markers": "python_version >= '3.8'", |             "markers": "python_version >= '3.8'", | ||||||
|             "version": "==0.27.2" |             "version": "==0.28.0" | ||||||
|         }, |         }, | ||||||
|         "idna": { |         "idna": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -1867,12 +1956,12 @@ | |||||||
|         }, |         }, | ||||||
|         "pytest": { |         "pytest": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", |                 "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", | ||||||
|                 "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" |                 "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "markers": "python_version >= '3.8'", |             "markers": "python_version >= '3.8'", | ||||||
|             "version": "==8.3.3" |             "version": "==8.3.4" | ||||||
|         }, |         }, | ||||||
|         "pytest-html": { |         "pytest-html": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -1901,12 +1990,42 @@ | |||||||
|         }, |         }, | ||||||
|         "tomli": { |         "tomli": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8", |                 "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", | ||||||
|                 "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391" |                 "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", | ||||||
|  |                 "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", | ||||||
|  |                 "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", | ||||||
|  |                 "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", | ||||||
|  |                 "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", | ||||||
|  |                 "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", | ||||||
|  |                 "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", | ||||||
|  |                 "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", | ||||||
|  |                 "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", | ||||||
|  |                 "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", | ||||||
|  |                 "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", | ||||||
|  |                 "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", | ||||||
|  |                 "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", | ||||||
|  |                 "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", | ||||||
|  |                 "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", | ||||||
|  |                 "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", | ||||||
|  |                 "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", | ||||||
|  |                 "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", | ||||||
|  |                 "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", | ||||||
|  |                 "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", | ||||||
|  |                 "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", | ||||||
|  |                 "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", | ||||||
|  |                 "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", | ||||||
|  |                 "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", | ||||||
|  |                 "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", | ||||||
|  |                 "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", | ||||||
|  |                 "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", | ||||||
|  |                 "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", | ||||||
|  |                 "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", | ||||||
|  |                 "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", | ||||||
|  |                 "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "markers": "python_version >= '3.8'", |             "markers": "python_version >= '3.8'", | ||||||
|             "version": "==2.1.0" |             "version": "==2.2.1" | ||||||
|         }, |         }, | ||||||
|         "tomlkit": { |         "tomlkit": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|   | |||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -1,13 +1,14 @@ | |||||||
| """Main app for backend api""" | """Main app for backend api""" | ||||||
|  |  | ||||||
| import logging | import logging | ||||||
| from fastapi import FastAPI, HTTPException | from fastapi import FastAPI, HTTPException, Query | ||||||
|  |  | ||||||
| from .structs.landmark import Landmark | from .structs.landmark import Landmark, Toilets | ||||||
| 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 .utils.landmarks_manager import LandmarkManager | ||||||
|  | from .utils.toilets_manager import ToiletsManager | ||||||
| from .utils.optimizer import Optimizer | from .utils.optimizer import Optimizer | ||||||
| from .utils.refiner import Refiner | from .utils.refiner import Refiner | ||||||
| from .persistence import client as cache_client | from .persistence import client as cache_client | ||||||
| @@ -36,19 +37,15 @@ def new_trip(preferences: Preferences, | |||||||
|         (uuid) : The uuid of the first landmark in the optimized route |         (uuid) : The uuid of the first landmark in the optimized route | ||||||
|     """ |     """ | ||||||
|     if preferences is None: |     if preferences is None: | ||||||
|         raise HTTPException(status_code=406, |         raise HTTPException(status_code=406, detail="Preferences not provided or incomplete.") | ||||||
|                             detail="Preferences not provided or incomplete.") |  | ||||||
|     if (preferences.shopping.score == 0 and |     if (preferences.shopping.score == 0 and | ||||||
|         preferences.sightseeing.score == 0 and |         preferences.sightseeing.score == 0 and | ||||||
|         preferences.nature.score == 0) : |         preferences.nature.score == 0) : | ||||||
|         raise HTTPException(status_code=406, |         raise HTTPException(status_code=406, detail="All preferences are 0.") | ||||||
|                             detail="All preferences are 0.") |  | ||||||
|     if start is None: |     if start is None: | ||||||
|         raise HTTPException(status_code=406, |         raise HTTPException(status_code=406, detail="Start coordinates not provided") | ||||||
|                             detail="Start coordinates not provided") |  | ||||||
|     if not (-90 <= start[0] <= 90 or -180 <= start[1] <= 180): |     if not (-90 <= start[0] <= 90 or -180 <= start[1] <= 180): | ||||||
|         raise HTTPException(status_code=423, |         raise HTTPException(status_code=422, detail="Start coordinates not in range") | ||||||
|                             detail="Start coordinates not in range") |  | ||||||
|     if end is None: |     if end is None: | ||||||
|         end = start |         end = start | ||||||
|         logger.info("No end coordinates provided. Using start=end.") |         logger.info("No end coordinates provided. Using start=end.") | ||||||
| @@ -134,5 +131,32 @@ 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: | ||||||
|         raise HTTPException(status_code=404, |         raise HTTPException(status_code=404, detail="Landmark not found") from exc | ||||||
|                             detail="Landmark not found") from exc |  | ||||||
|  |  | ||||||
|  | @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 | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| """Module used for handling cache""" | """Module used for handling cache""" | ||||||
|  | from pymemcache import serde | ||||||
| from pymemcache.client.base import Client | from pymemcache.client.base import Client | ||||||
|  |  | ||||||
| from .constants import MEMCACHED_HOST_PATH | from .constants import MEMCACHED_HOST_PATH | ||||||
| @@ -70,5 +70,6 @@ else: | |||||||
|         MEMCACHED_HOST_PATH, |         MEMCACHED_HOST_PATH, | ||||||
|         timeout=1, |         timeout=1, | ||||||
|         allow_unicode_keys=True, |         allow_unicode_keys=True, | ||||||
|         encoding='utf-8' |         encoding='utf-8',  | ||||||
|  |         serde=serde.pickle_serde | ||||||
|     ) |     ) | ||||||
|   | |||||||
							
								
								
									
										4442
									
								
								backend/src/sandbox/bandung_data.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4442
									
								
								backend/src/sandbox/bandung_data.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										698
									
								
								backend/src/sandbox/colmar_data.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										698
									
								
								backend/src/sandbox/colmar_data.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,698 @@ | |||||||
|  | { | ||||||
|  |     "type": "FeatureCollection", | ||||||
|  |     "generator": "overpass-turbo", | ||||||
|  |     "copyright": "The data included in this document is from www.openstreetmap.org. The data is made available under ODbL.", | ||||||
|  |     "timestamp": "2024-12-02T21:14:59Z", | ||||||
|  |     "features": [ | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/1345741798", | ||||||
|  |           "name": "Cordonnerie Saint-Joseph", | ||||||
|  |           "shop": "shoes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3481705, | ||||||
|  |             48.0816462 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/1345741798" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/2659184738", | ||||||
|  |           "brand": "Armand Thiery", | ||||||
|  |           "brand:wikidata": "Q2861975", | ||||||
|  |           "brand:wikipedia": "fr:Armand Thiery", | ||||||
|  |           "name": "Armand Thiery", | ||||||
|  |           "opening_hours": "Mo-Sa 09:30-19:00", | ||||||
|  |           "shop": "clothes", | ||||||
|  |           "wheelchair": "limited" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3594454, | ||||||
|  |             48.0785574 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/2659184738" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/3618136290", | ||||||
|  |           "name": "Chez Dominique", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3362362, | ||||||
|  |             48.0712174 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/3618136290" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/3618136605", | ||||||
|  |           "name": "Divamod", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3304253, | ||||||
|  |             48.0782989 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/3618136605" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/3618284507", | ||||||
|  |           "name": "Star tendances et voyages", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3474029, | ||||||
|  |             48.0830993 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/3618284507" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/3619696125", | ||||||
|  |           "brand": "Zeeman", | ||||||
|  |           "brand:wikidata": "Q184399", | ||||||
|  |           "name": "Zeeman", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3413834, | ||||||
|  |             48.0638444 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/3619696125" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/4594398129", | ||||||
|  |           "name": "Miss et Mister", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3308309, | ||||||
|  |             48.0779118 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/4594398129" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/4907320441", | ||||||
|  |           "brand": "Sergent Major", | ||||||
|  |           "brand:wikidata": "Q62521738", | ||||||
|  |           "clothes": "babies;children", | ||||||
|  |           "name": "Sergent Major", | ||||||
|  |           "opening_hours": "Mo-Sa 09:30-19:00", | ||||||
|  |           "shop": "clothes", | ||||||
|  |           "wheelchair": "no" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.359116, | ||||||
|  |             48.0787229 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/4907320441" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/4907364791", | ||||||
|  |           "brand": "Armand Thiery", | ||||||
|  |           "brand:wikidata": "Q2861975", | ||||||
|  |           "brand:wikipedia": "fr:Armand Thiery", | ||||||
|  |           "clothes": "women", | ||||||
|  |           "name": "Armand Thiery", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3601857, | ||||||
|  |             48.0783373 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/4907364791" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/4907385675", | ||||||
|  |           "check_date": "2024-05-19", | ||||||
|  |           "clothes": "children", | ||||||
|  |           "name": "Du Pareil...au même", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3604521, | ||||||
|  |             48.0779726 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/4907385675" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/4922191645", | ||||||
|  |           "name": "Abilos", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3566167, | ||||||
|  |             48.0794136 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/4922191645" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/4922191648", | ||||||
|  |           "brand": "Esprit", | ||||||
|  |           "brand:wikidata": "Q532746", | ||||||
|  |           "brand:wikipedia": "en:Esprit Holdings", | ||||||
|  |           "name": "Esprit", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3554004, | ||||||
|  |             48.0787549 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/4922191648" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/4922191972", | ||||||
|  |           "brand": "Guess", | ||||||
|  |           "brand:wikidata": "Q2470307", | ||||||
|  |           "brand:wikipedia": "en:Guess (clothing)", | ||||||
|  |           "name": "Guess", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.355273, | ||||||
|  |             48.0788003 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/4922191972" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/4922192001", | ||||||
|  |           "name": "Lingerie", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3575453, | ||||||
|  |             48.0779317 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/4922192001" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/5359915869", | ||||||
|  |           "name": "Al Assil", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3305665, | ||||||
|  |             48.0780902 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/5359915869" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/9089360040", | ||||||
|  |           "brand": "Grain de Malice", | ||||||
|  |           "brand:wikidata": "Q66757157", | ||||||
|  |           "clothes": "women", | ||||||
|  |           "name": "Grain de Malice", | ||||||
|  |           "shop": "clothes", | ||||||
|  |           "short_name": "GDM" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3593125, | ||||||
|  |             48.0786234 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/9089360040" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/9095193153", | ||||||
|  |           "brand": "Undiz", | ||||||
|  |           "brand:wikidata": "Q105306275", | ||||||
|  |           "clothes": "underwear", | ||||||
|  |           "name": "Undiz", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3599579, | ||||||
|  |             48.0782846 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/9095193153" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/9095193154", | ||||||
|  |           "branch": "Lingerie", | ||||||
|  |           "brand": "RougeGorge", | ||||||
|  |           "brand:wikidata": "Q104600739", | ||||||
|  |           "clothes": "underwear", | ||||||
|  |           "name": "RougeGorge", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3604883, | ||||||
|  |             48.0781607 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/9095193154" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/9095212690", | ||||||
|  |           "alt_name": "North Face", | ||||||
|  |           "brand": "The North Face", | ||||||
|  |           "brand:wikidata": "Q152784", | ||||||
|  |           "brand:wikipedia": "en:The North Face", | ||||||
|  |           "check_date": "2024-05-19", | ||||||
|  |           "name": "The North Face", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3603923, | ||||||
|  |             48.0773727 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/9095212690" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/9095270059", | ||||||
|  |           "air_conditioning": "no", | ||||||
|  |           "clothes": "men", | ||||||
|  |           "level": "0", | ||||||
|  |           "name": "Maison Aume", | ||||||
|  |           "second_hand": "no", | ||||||
|  |           "shop": "clothes", | ||||||
|  |           "wheelchair": "no" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.361364, | ||||||
|  |             48.0799999 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/9095270059" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/9098624272", | ||||||
|  |           "name": "Destock Place", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3575161, | ||||||
|  |             48.0793009 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/9098624272" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/9123861652", | ||||||
|  |           "name": "Weackers", | ||||||
|  |           "shop": "shoes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.361329, | ||||||
|  |             48.0785972 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/9123861652" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/9162179887", | ||||||
|  |           "brand": "Calzedonia", | ||||||
|  |           "brand:wikidata": "Q1027874", | ||||||
|  |           "brand:wikipedia": "en:Calzedonia", | ||||||
|  |           "name": "Calzedonia", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3606374, | ||||||
|  |             48.0780809 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/9162179887" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/9162206449", | ||||||
|  |           "clothes": "women", | ||||||
|  |           "name": "Cop. Copine", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3600947, | ||||||
|  |             48.078399 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/9162206449" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/9162226360", | ||||||
|  |           "brand": "Okaïdi", | ||||||
|  |           "brand:wikidata": "Q3350027", | ||||||
|  |           "brand:wikipedia": "fr:Okaïdi", | ||||||
|  |           "name": "Okaïdi", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3596986, | ||||||
|  |             48.078428 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/9162226360" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/9162227010", | ||||||
|  |           "brand": "Jules", | ||||||
|  |           "brand:wikidata": "Q3188386", | ||||||
|  |           "brand:wikipedia": "fr:Jules (enseigne)", | ||||||
|  |           "clothes": "men", | ||||||
|  |           "name": "Jules", | ||||||
|  |           "opening_hours": "Mo-Sa 09:30-19:00", | ||||||
|  |           "phone": "+33 3 89 41 03 62", | ||||||
|  |           "shop": "clothes", | ||||||
|  |           "website": "https://www.jules.com/fr-fr/magasins/1600133/" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3600323, | ||||||
|  |             48.0782229 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/9162227010" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/10151865029", | ||||||
|  |           "name": "Atelier Cinq", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3571756, | ||||||
|  |             48.0772657 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/10151865029" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/10862176110", | ||||||
|  |           "name": "L'hexagone", | ||||||
|  |           "shop": "bag" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3808571, | ||||||
|  |             48.0814138 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/10862176110" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/11150877331", | ||||||
|  |           "brand": "Punt Roma", | ||||||
|  |           "brand:wikidata": "Q101423290", | ||||||
|  |           "clothes": "women", | ||||||
|  |           "name": "Punt Roma", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3571859, | ||||||
|  |             48.0779406 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/11150877331" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/11150959880", | ||||||
|  |           "name": "Caroll", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3579354, | ||||||
|  |             48.0779291 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/11150959880" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/11302242094", | ||||||
|  |           "branch": "Wintzenheim", | ||||||
|  |           "name": "Label Fripe", | ||||||
|  |           "opening_hours": "Mo-Sa 09:00-18:45", | ||||||
|  |           "phone": "+33 3 89 27 39 25", | ||||||
|  |           "second_hand": "only", | ||||||
|  |           "shop": "clothes", | ||||||
|  |           "website": "https://labelfripe.fr/label-fripe-wintzenheim/" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3109899, | ||||||
|  |             48.0850362 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/11302242094" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/11392247003", | ||||||
|  |           "name": "Lingerie Sipp", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3111507, | ||||||
|  |             48.0841835 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/11392247003" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/11778819781", | ||||||
|  |           "addr:city": "Colmar", | ||||||
|  |           "addr:housenumber": "10", | ||||||
|  |           "addr:postcode": "68000", | ||||||
|  |           "addr:street": "Rue des Têtes", | ||||||
|  |           "clothes": "suits;hats;men", | ||||||
|  |           "name": "Phillipe", | ||||||
|  |           "phone": "0389411983", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3559389, | ||||||
|  |             48.0789064 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/11778819781" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/11799215969", | ||||||
|  |           "brand": "Petit Bateau", | ||||||
|  |           "brand:wikidata": "Q3377090", | ||||||
|  |           "name": "Petit Bateau", | ||||||
|  |           "opening_hours": "Mo-Sa 10:00-19:00; Su 10:00-18:00", | ||||||
|  |           "phone": "+33 3 89 24 97 85", | ||||||
|  |           "shop": "clothes", | ||||||
|  |           "website": "https://stores.petit-bateau.com/france/colmar/9-rue-des-boulangers" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.355149, | ||||||
|  |             48.0780213 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/11799215969" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/11816704669", | ||||||
|  |           "addr:housenumber": "10", | ||||||
|  |           "addr:street": "Rue des Boulangers", | ||||||
|  |           "name": "des petits hauts", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3555001, | ||||||
|  |             48.0780768 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/11816704669" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/12320343534", | ||||||
|  |           "addr:city": "Colmar", | ||||||
|  |           "addr:housenumber": "44", | ||||||
|  |           "addr:postcode": "68000", | ||||||
|  |           "addr:street": "Rue des Clefs", | ||||||
|  |           "brand": "Un Jour Ailleurs", | ||||||
|  |           "brand:wikidata": "Q105106211", | ||||||
|  |           "clothes": "women", | ||||||
|  |           "name": "Un Jour Ailleurs", | ||||||
|  |           "opening_hours": "Mo-Fr 10:00-19:00; Sa 10:00-18:30", | ||||||
|  |           "phone": "+33368318572", | ||||||
|  |           "shop": "clothes", | ||||||
|  |           "website": "https://boutique.unjourailleurs.com/fr/mode-femme/boutique-colmar-76" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.35897, | ||||||
|  |             48.0789807 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/12320343534" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "type": "Feature", | ||||||
|  |         "properties": { | ||||||
|  |           "@id": "node/12320343536", | ||||||
|  |           "addr:city": "Colmar", | ||||||
|  |           "addr:housenumber": "38", | ||||||
|  |           "addr:postcode": "68000", | ||||||
|  |           "addr:street": "Rue des Clefs", | ||||||
|  |           "brand": "Timberland", | ||||||
|  |           "brand:wikidata": "Q1539185", | ||||||
|  |           "name": "Timberland", | ||||||
|  |           "opening_hours": "Mo-Sa 10:00-19:00", | ||||||
|  |           "phone": "+33389298650", | ||||||
|  |           "shop": "clothes" | ||||||
|  |         }, | ||||||
|  |         "geometry": { | ||||||
|  |           "type": "Point", | ||||||
|  |           "coordinates": [ | ||||||
|  |             7.3592409, | ||||||
|  |             48.0788785 | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "id": "node/12320343536" | ||||||
|  |       } | ||||||
|  |     ] | ||||||
|  |   } | ||||||
							
								
								
									
										350
									
								
								backend/src/sandbox/get_streets.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										350
									
								
								backend/src/sandbox/get_streets.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,350 @@ | |||||||
|  | # pylint: skip-file | ||||||
|  |  | ||||||
|  | import numpy as np | ||||||
|  | import json | ||||||
|  | import os | ||||||
|  | from typing import Optional, Literal | ||||||
|  | from sklearn.cluster import DBSCAN | ||||||
|  | from sklearn.decomposition import PCA | ||||||
|  | import matplotlib.pyplot as plt | ||||||
|  | from pydantic import BaseModel | ||||||
|  | from OSMPythonTools.overpass import Overpass, overpassQueryBuilder | ||||||
|  | from OSMPythonTools.cachingStrategy import CachingStrategy, JSON | ||||||
|  | from math import sin, cos, sqrt, atan2, radians | ||||||
|  |  | ||||||
|  |  | ||||||
|  | EARTH_RADIUS_KM = 6373 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ShoppingLocation(BaseModel): | ||||||
|  |     type: Literal['street', 'area'] | ||||||
|  |     importance: int | ||||||
|  |     centroid: tuple | ||||||
|  |     start: Optional[list] = None | ||||||
|  |     end: Optional[list] = None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Output to frontend | ||||||
|  | class Landmark(BaseModel) : | ||||||
|  |     # Properties of the landmark | ||||||
|  |     name : str | ||||||
|  |     type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish'] | ||||||
|  |     location : tuple | ||||||
|  |     osm_type : str | ||||||
|  |     osm_id : int | ||||||
|  |     attractiveness : int | ||||||
|  |     n_tags : int | ||||||
|  |     image_url : Optional[str] = None | ||||||
|  |     website_url : Optional[str] = None | ||||||
|  |     description : Optional[str] = None                          # TODO future | ||||||
|  |     duration : Optional[int] = 0 | ||||||
|  |     name_en : Optional[str] = None | ||||||
|  |  | ||||||
|  |     # Additional properties depending on specific tour | ||||||
|  |     must_do : Optional[bool] = False | ||||||
|  |     must_avoid : Optional[bool] = False | ||||||
|  |     is_secondary : Optional[bool] = False | ||||||
|  |  | ||||||
|  |     time_to_reach_next : Optional[int] = 0 | ||||||
|  |     next_uuid : Optional[str] = None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def extract_points(filestr: str) : | ||||||
|  |     """ | ||||||
|  |     Extract points from geojson file. | ||||||
|  |  | ||||||
|  |     Returns : | ||||||
|  |         np.array containing the points | ||||||
|  |     """ | ||||||
|  |     points = [] | ||||||
|  |  | ||||||
|  |     with open(os.path.dirname(__file__) + '/' + filestr, 'r') as f: | ||||||
|  |         geojson = json.load(f) | ||||||
|  |  | ||||||
|  |         for feature in geojson['features']: | ||||||
|  |             if feature['geometry']['type'] == 'Point': | ||||||
|  |                 centroid = feature['geometry']['coordinates'] | ||||||
|  |                 points.append(centroid) | ||||||
|  |  | ||||||
|  |             elif feature['geometry']['type'] == 'Polygon': | ||||||
|  |                 centroid = np.array(feature['geometry']['coordinates'][0][0]) | ||||||
|  |                 points.append(centroid) | ||||||
|  |      | ||||||
|  |     # Convert the list of points to a NumPy array | ||||||
|  |     return np.array(points) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_distance(p1: tuple[float, float], p2: tuple[float, float]) -> int: | ||||||
|  |     """ | ||||||
|  |     Calculate the time in minutes to travel from one location to another. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         p1 (tuple[float, float]): Coordinates of the starting location. | ||||||
|  |         p2 (tuple[float, float]): Coordinates of the destination. | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |         int: Time to travel from p1 to p2 in minutes. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     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]) | ||||||
|  |  | ||||||
|  |         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)) | ||||||
|  |  | ||||||
|  |         return EARTH_RADIUS_KM * c | ||||||
|  |  | ||||||
|  | def filter_clusters(cluster_points, cluster_labels): | ||||||
|  |     """ | ||||||
|  |     Remove clusters of less importance. | ||||||
|  |     """ | ||||||
|  |     label_counts = np.bincount(cluster_labels) | ||||||
|  |  | ||||||
|  |     # Step 3: Get the indices (labels) of the 5 largest clusters | ||||||
|  |     top_5_labels = np.argsort(label_counts)[-5:]  # Get the largest 5 clusters | ||||||
|  |  | ||||||
|  |     # Step 4: Filter points to keep only the points in the top 5 clusters | ||||||
|  |     filtered_cluster_points = [] | ||||||
|  |     filtered_cluster_labels = [] | ||||||
|  |  | ||||||
|  |     for label in top_5_labels: | ||||||
|  |         filtered_cluster_points.append(cluster_points[cluster_labels == label]) | ||||||
|  |         filtered_cluster_labels.append(np.full((label_counts[label],), label))  # Replicate the label | ||||||
|  |  | ||||||
|  |     # Concatenate filtered clusters into a single array | ||||||
|  |     return np.vstack(filtered_cluster_points), np.concatenate(filtered_cluster_labels) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def fit_lines(points, labels): | ||||||
|  |     """ | ||||||
|  |     Fit lines to identified clusters. | ||||||
|  |     """ | ||||||
|  |     all_x = [] | ||||||
|  |     all_y = [] | ||||||
|  |     lines = [] | ||||||
|  |     locations = [] | ||||||
|  |  | ||||||
|  |     for label in set(labels): | ||||||
|  |         cluster_points = points[labels == label] | ||||||
|  |  | ||||||
|  |         # If there's not enough points, skip | ||||||
|  |         if len(cluster_points) < 2: | ||||||
|  |             continue | ||||||
|  |  | ||||||
|  |         # Apply PCA to find the principal component (i.e., the line of best fit) | ||||||
|  |         pca = PCA(n_components=1) | ||||||
|  |         pca.fit(cluster_points) | ||||||
|  |  | ||||||
|  |         direction = pca.components_[0] | ||||||
|  |         centroid = pca.mean_ | ||||||
|  |  | ||||||
|  |         # Project the cluster points onto the principal direction (line direction) | ||||||
|  |         projections = np.dot(cluster_points - centroid, direction) | ||||||
|  |  | ||||||
|  |         # Get the range of the projections to find the approximate length of the cluster | ||||||
|  |         cluster_length = projections.max() - projections.min() | ||||||
|  |  | ||||||
|  |         # Now adjust `t` so that it scales with the cluster length | ||||||
|  |         t = np.linspace(-cluster_length / 2.75, cluster_length / 2.75, 10) | ||||||
|  |  | ||||||
|  |         # Calculate the start and end of the line based on min/max projections | ||||||
|  |         start_point = centroid[0] + t*direction[0] | ||||||
|  |         end_point = centroid[1] + t*direction[1] | ||||||
|  |          | ||||||
|  |         # Store the line | ||||||
|  |         lines.append((start_point, end_point)) | ||||||
|  |  | ||||||
|  |         # For visualization, store the points | ||||||
|  |         all_x.append(min(start_point)) | ||||||
|  |         all_x.append(max(start_point)) | ||||||
|  |         all_y.append(min(end_point)) | ||||||
|  |         all_y.append(max(end_point)) | ||||||
|  |  | ||||||
|  |         if np.linalg.norm(t) <= 0.0045 : | ||||||
|  |             loc = ShoppingLocation( | ||||||
|  |                 type='area', | ||||||
|  |                 centroid=tuple((centroid[1], centroid[0])), | ||||||
|  |                 importance = len(cluster_points), | ||||||
|  |             ) | ||||||
|  |         else : | ||||||
|  |             loc = ShoppingLocation( | ||||||
|  |                 type='street', | ||||||
|  |                 centroid=tuple((centroid[1], centroid[0])), | ||||||
|  |                 importance = len(cluster_points), | ||||||
|  |                 start=start_point, | ||||||
|  |                 end=end_point | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         locations.append(loc) | ||||||
|  |  | ||||||
|  |     xmin = min(all_x) | ||||||
|  |     xmax = max(all_x) | ||||||
|  |     ymin = min(all_y) | ||||||
|  |     ymax = max(all_y) | ||||||
|  |     corners = (xmin, xmax, ymin, ymax) | ||||||
|  |  | ||||||
|  |     return corners, locations | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def create_landmark(shopping_location: ShoppingLocation): | ||||||
|  |  | ||||||
|  |     # Define the bounding box for a given radius around the coordinates | ||||||
|  |     lat, lon = shopping_location.centroid | ||||||
|  |     bbox = ("around:1000", str(lat), str(lon)) | ||||||
|  |      | ||||||
|  |     overpass = Overpass() | ||||||
|  |     # CachingStrategy.use(JSON, cacheDir=OSM_CACHE_DIR) | ||||||
|  |  | ||||||
|  |     # Query neighborhoods and shopping malls | ||||||
|  |     selectors = ['"place"~"^(suburb|neighborhood|neighbourhood|quarter|city_block)$"', '"shop"="mall"'] | ||||||
|  |  | ||||||
|  |     min_dist = float('inf') | ||||||
|  |     new_name = 'Shopping Area' | ||||||
|  |     new_name_en = None | ||||||
|  |     osm_id = 0 | ||||||
|  |     osm_type = 'node' | ||||||
|  |  | ||||||
|  |     for sel in selectors :  | ||||||
|  |         query = overpassQueryBuilder( | ||||||
|  |             bbox = bbox, | ||||||
|  |             elementType = ['node', 'way', 'relation'], | ||||||
|  |             selector = sel, | ||||||
|  |             includeCenter = True, | ||||||
|  |             out = 'center' | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             result = overpass.query(query) | ||||||
|  |         except Exception as e: | ||||||
|  |             raise Exception("query unsuccessful") | ||||||
|  |  | ||||||
|  |         for elem in result.elements(): | ||||||
|  |  | ||||||
|  |             location = (elem.centerLat(), elem.centerLon()) | ||||||
|  |  | ||||||
|  |             if location[0] is None :  | ||||||
|  |                 location = (elem.lat(), elem.lon()) | ||||||
|  |                 if location[0] is None :  | ||||||
|  |                     # print(f"Fetching coordinates failed with {elem.type()}/{elem.id()}") | ||||||
|  |                     continue | ||||||
|  |  | ||||||
|  |             # print(f"Distance : {get_distance(shopping_location.centroid, location)}") | ||||||
|  |             d = get_distance(shopping_location.centroid, location) | ||||||
|  |             if  d < min_dist : | ||||||
|  |                 min_dist = d | ||||||
|  |                 new_name = elem.tag('name') | ||||||
|  |                 osm_type = elem.type()              # Add type: 'way' or 'relation' | ||||||
|  |                 osm_id = elem.id()                  # Add OSM id  | ||||||
|  |  | ||||||
|  |                 # add english name if it exists | ||||||
|  |                 try : | ||||||
|  |                     new_name_en = elem.tag('name:en') | ||||||
|  |                 except: | ||||||
|  |                     pass  | ||||||
|  |      | ||||||
|  |     return Landmark( | ||||||
|  |         name=new_name, | ||||||
|  |         type='shopping', | ||||||
|  |         location=shopping_location.centroid,              # TODO: use the fact the we can also recognize streets. | ||||||
|  |         attractiveness=shopping_location.importance, | ||||||
|  |         n_tags=0, | ||||||
|  |         osm_id=osm_id, | ||||||
|  |         osm_type=osm_type, | ||||||
|  |         name_en=new_name_en | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Extract points | ||||||
|  | points = extract_points('vienna_data.json') | ||||||
|  |  | ||||||
|  | # print(len(points)) | ||||||
|  |  | ||||||
|  | ######## Create a figure with 1 row and 3 columns for side-by-side plots | ||||||
|  | fig, axes = plt.subplots(1, 3, figsize=(15, 5)) | ||||||
|  | # Plot Raw data points | ||||||
|  | axes[0].set_title('Raw Data') | ||||||
|  | axes[0].scatter(points[:, 0], points[:, 1], color='blue', s=20) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Apply DBSCAN to find clusters. Choose different settings for different cities. | ||||||
|  | if len(points) > 400 : | ||||||
|  |     dbscan = DBSCAN(eps=0.00118, min_samples=15, algorithm='kd_tree')  # for large cities | ||||||
|  | else : | ||||||
|  |     dbscan = DBSCAN(eps=0.00075, min_samples=10, algorithm='kd_tree')  # for small cities | ||||||
|  |  | ||||||
|  | labels = dbscan.fit_predict(points) | ||||||
|  |  | ||||||
|  | # Separate clustered points and noise points | ||||||
|  | clustered_points = points[labels != -1] | ||||||
|  | clustered_labels = labels[labels != -1] | ||||||
|  | noise_points = points[labels == -1]  | ||||||
|  |  | ||||||
|  | ######## Plot n°1: DBSCAN Clustering Results | ||||||
|  | axes[1].set_title('DBSCAN Clusters') | ||||||
|  | axes[1].scatter(clustered_points[:, 0], clustered_points[:, 1], c=clustered_labels, cmap='rainbow', s=20) | ||||||
|  | axes[1].scatter(noise_points[:, 0], noise_points[:, 1], c='blue', s=7, label='Noise') | ||||||
|  |  | ||||||
|  | # Keep the 5 biggest clusters | ||||||
|  | clustered_points, clustered_labels = filter_clusters(clustered_points, clustered_labels) | ||||||
|  |  | ||||||
|  | # Fit lines | ||||||
|  | corners, locations = fit_lines(clustered_points, clustered_labels) | ||||||
|  | (xmin, xmax, ymin, ymax) = corners | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ######## Plot clustered points in normal size and noise points separately | ||||||
|  | axes[2].scatter(clustered_points[:, 0], clustered_points[:, 1], c=clustered_labels, cmap='rainbow', s=30) | ||||||
|  | axes[2].set_title('PCA Fitted Lines on Clusters') | ||||||
|  |  | ||||||
|  | # Create a list of Landmarks for the shopping things | ||||||
|  | shopping_landmarks = [] | ||||||
|  | for loc in locations : | ||||||
|  |     axes[2].scatter(loc.centroid[1], loc.centroid[0], color='red', marker='x', s=200, linewidth=3) | ||||||
|  |     landmark = create_landmark(loc) | ||||||
|  |     shopping_landmarks.append(landmark) | ||||||
|  |     axes[2].text(loc.centroid[1], loc.centroid[0], landmark.name,  | ||||||
|  |              ha='center', va='top', fontsize=6,  | ||||||
|  |              bbox=dict(facecolor='white', edgecolor='black', boxstyle='round,pad=0.2'), | ||||||
|  |              zorder=3) | ||||||
|  |      | ||||||
|  |      | ||||||
|  |  | ||||||
|  | ####### Plot the detected lines in the final plot ####### | ||||||
|  | # for loc in locations: | ||||||
|  | #     if loc.type == 'street' : | ||||||
|  | #         line_x = loc.start | ||||||
|  | #         line_y = loc.end | ||||||
|  | #         axes[2].plot(line_x, line_y, color='lime', linewidth=3) | ||||||
|  | #     else : | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | axes[0].set_xlim(xmin-0.01, xmax+0.01) | ||||||
|  | axes[0].set_ylim(ymin-0.01, ymax+0.01) | ||||||
|  |  | ||||||
|  | axes[1].set_xlim(xmin-0.01, xmax+0.01) | ||||||
|  | axes[1].set_ylim(ymin-0.01, ymax+0.01) | ||||||
|  |  | ||||||
|  | axes[2].set_xlim(xmin-0.01, xmax+0.01) | ||||||
|  | axes[2].set_ylim(ymin-0.01, ymax+0.01) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | print("\n\n\n") | ||||||
|  | for landmark in shopping_landmarks : | ||||||
|  |     print(f"{landmark.name} is a shopping area with a score of {landmark.attractiveness}") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | plt.tight_layout() | ||||||
|  | plt.show() | ||||||
							
								
								
									
										17824
									
								
								backend/src/sandbox/lyon_data.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17824
									
								
								backend/src/sandbox/lyon_data.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										42085
									
								
								backend/src/sandbox/newyork_data.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42085
									
								
								backend/src/sandbox/newyork_data.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										83615
									
								
								backend/src/sandbox/paris_data.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83615
									
								
								backend/src/sandbox/paris_data.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										4947
									
								
								backend/src/sandbox/strasbourg_data.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4947
									
								
								backend/src/sandbox/strasbourg_data.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										23140
									
								
								backend/src/sandbox/vienna_data.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23140
									
								
								backend/src/sandbox/vienna_data.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										2844
									
								
								backend/src/sandbox/winterthur_data.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2844
									
								
								backend/src/sandbox/winterthur_data.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										16070
									
								
								backend/src/sandbox/zurich_data.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16070
									
								
								backend/src/sandbox/zurich_data.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -73,8 +73,6 @@ class Landmark(BaseModel) : | |||||||
|         t_to_next_str = f", time_to_next={self.time_to_reach_next}" if self.time_to_reach_next else "" |         t_to_next_str = f", time_to_next={self.time_to_reach_next}" if self.time_to_reach_next else "" | ||||||
|         is_secondary_str = ", secondary" if self.is_secondary else "" |         is_secondary_str = ", secondary" if self.is_secondary else "" | ||||||
|         type_str = '(' + self.type + ')' |         type_str = '(' + self.type + ')' | ||||||
|         if self.type in ["start", "finish", "nature", "shopping"] : |  | ||||||
|             type_str += '\t ' |  | ||||||
|  |  | ||||||
|         return (f'Landmark{type_str}: [{self.name} @{self.location}, ' |         return (f'Landmark{type_str}: [{self.name} @{self.location}, ' | ||||||
|                 f'score={self.attractiveness}{t_to_next_str}{is_secondary_str}]') |                 f'score={self.attractiveness}{t_to_next_str}{is_secondary_str}]') | ||||||
| @@ -117,3 +115,28 @@ 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}' | ||||||
|  |      | ||||||
|  |     class Config: | ||||||
|  |         # This allows us to easily convert the model to and from dictionaries | ||||||
|  |         orm_mode = True | ||||||
							
								
								
									
										42
									
								
								backend/src/tests/test_cache.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								backend/src/tests/test_cache.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | """Collection of tests to ensure correct handling of invalid input.""" | ||||||
|  |  | ||||||
|  | from fastapi.testclient import TestClient | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .test_utils import load_trip_landmarks | ||||||
|  | from ..main import app | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.fixture(scope="module") | ||||||
|  | def client(): | ||||||
|  |     """Client used to call the app.""" | ||||||
|  |     return TestClient(app) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_cache(client, request):    # pylint: disable=redefined-outer-name | ||||||
|  |     """ | ||||||
|  |     Test n°1 : Custom test in Turckheim to ensure small villages are also supported. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         client: | ||||||
|  |         request: | ||||||
|  |     """ | ||||||
|  |     duration_minutes = 15 | ||||||
|  |     response = client.post( | ||||||
|  |         "/trip/new", | ||||||
|  |         json={ | ||||||
|  |             "preferences": {"sightseeing": {"type": "sightseeing", "score": 5}, | ||||||
|  |             "nature": {"type": "nature", "score": 5}, | ||||||
|  |             "shopping": {"type": "shopping", "score": 5}, | ||||||
|  |             "max_time_minute": duration_minutes, | ||||||
|  |             "detour_tolerance_minute": 0}, | ||||||
|  |             "start": [48.084588, 7.280405] | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     result = response.json() | ||||||
|  |     landmarks = load_trip_landmarks(client, result['first_landmark_uuid']) | ||||||
|  |     landmarks_cached = load_trip_landmarks(client, result['first_landmark_uuid'], True) | ||||||
|  |  | ||||||
|  |     # checks : | ||||||
|  |     assert response.status_code == 200  # check for successful planning | ||||||
|  |     assert landmarks_cached == landmarks | ||||||
| @@ -33,19 +33,19 @@ def invalid_client(): | |||||||
|         ([91, 181], {"sightseeing": {"type": "nature", "score": 5}, |         ([91, 181], {"sightseeing": {"type": "nature", "score": 5}, | ||||||
|                      "nature": {"type": "nature", "score": 5}, |                      "nature": {"type": "nature", "score": 5}, | ||||||
|                      "shopping": {"type": "shopping", "score": 5}, |                      "shopping": {"type": "shopping", "score": 5}, | ||||||
|                     }, 423), |                     }, 422), | ||||||
|         ([-91, 181], {"sightseeing": {"type": "nature", "score": 5}, |         ([-91, 181], {"sightseeing": {"type": "nature", "score": 5}, | ||||||
|                      "nature": {"type": "nature", "score": 5}, |                      "nature": {"type": "nature", "score": 5}, | ||||||
|                      "shopping": {"type": "shopping", "score": 5}, |                      "shopping": {"type": "shopping", "score": 5}, | ||||||
|                     }, 423), |                     }, 422), | ||||||
|         ([91, -181], {"sightseeing": {"type": "nature", "score": 5}, |         ([91, -181], {"sightseeing": {"type": "nature", "score": 5}, | ||||||
|                      "nature": {"type": "nature", "score": 5}, |                      "nature": {"type": "nature", "score": 5}, | ||||||
|                      "shopping": {"type": "shopping", "score": 5}, |                      "shopping": {"type": "shopping", "score": 5}, | ||||||
|                     }, 423), |                     }, 422), | ||||||
|         ([-91, -181], {"sightseeing": {"type": "nature", "score": 5}, |         ([-91, -181], {"sightseeing": {"type": "nature", "score": 5}, | ||||||
|                      "nature": {"type": "nature", "score": 5}, |                      "nature": {"type": "nature", "score": 5}, | ||||||
|                      "shopping": {"type": "shopping", "score": 5}, |                      "shopping": {"type": "shopping", "score": 5}, | ||||||
|                     }, 423), |                     }, 422), | ||||||
|     ] |     ] | ||||||
| ) | ) | ||||||
| def test_input(invalid_client, start, preferences, status_code):   # pylint: disable=redefined-outer-name | def test_input(invalid_client, start, preferences, status_code):   # pylint: disable=redefined-outer-name | ||||||
|   | |||||||
| @@ -78,6 +78,36 @@ def test_bellecour(client, request) :   # pylint: disable=redefined-outer-name | |||||||
|     assert 136200148 in osm_ids         # check for Cathédrale St. Jean in trip |     assert 136200148 in osm_ids         # check for Cathédrale St. Jean in trip | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_shopping(client, request) :   # pylint: disable=redefined-outer-name | ||||||
|  |     """ | ||||||
|  |     Test n°3 : Custom test in Lyon centre to ensure shopping clusters are found. | ||||||
|  |      | ||||||
|  |     Args: | ||||||
|  |         client: | ||||||
|  |         request: | ||||||
|  |     """ | ||||||
|  |     duration_minutes = 600 | ||||||
|  |     response = client.post( | ||||||
|  |         "/trip/new", | ||||||
|  |         json={ | ||||||
|  |             "preferences": {"sightseeing": {"type": "sightseeing", "score": 0}, | ||||||
|  |                             "nature": {"type": "nature", "score": 0}, | ||||||
|  |                             "shopping": {"type": "shopping", "score": 5}, | ||||||
|  |                             "max_time_minute": duration_minutes, | ||||||
|  |                             "detour_tolerance_minute": 0}, | ||||||
|  |             "start": [45.7576485, 4.8330241] | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     result = response.json() | ||||||
|  |     landmarks = load_trip_landmarks(client, result['first_landmark_uuid']) | ||||||
|  |     # osm_ids = landmarks_to_osmid(landmarks) | ||||||
|  |  | ||||||
|  |     # Add details to report | ||||||
|  |     log_trip_details(request, landmarks, result['total_time'], duration_minutes) | ||||||
|  |  | ||||||
|  |     # checks : | ||||||
|  |     assert response.status_code == 200  # check for successful planning | ||||||
|  |     assert duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2 | ||||||
|  |  | ||||||
| # def test_new_trip_single_prefs(client): | # def test_new_trip_single_prefs(client): | ||||||
| #     response = client.post( | #     response = client.post( | ||||||
|   | |||||||
							
								
								
									
										102
									
								
								backend/src/tests/test_toilets.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								backend/src/tests/test_toilets.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | |||||||
|  | """Collection of tests to ensure correct implementation and track progress. """ | ||||||
|  |  | ||||||
|  | from fastapi.testclient import TestClient | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from ..structs.landmark import Toilets | ||||||
|  | from ..main import app | ||||||
|  |  | ||||||
|  | @pytest.fixture(scope="module") | ||||||
|  | def client(): | ||||||
|  |     """Client used to call the app.""" | ||||||
|  |     return TestClient(app) | ||||||
|  |  | ||||||
|  | @pytest.mark.parametrize( | ||||||
|  |     "location,radius,status_code", | ||||||
|  |     [ | ||||||
|  |         ({}, None, 422),                # Invalid case: no location at all. | ||||||
|  |         ([443], None, 422),             # Invalid cases: invalid location. | ||||||
|  |         ([443, 433], None, 422),             # Invalid cases: invalid location. | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  | def test_invalid_input(client, location, radius, status_code):    # pylint: disable=redefined-outer-name | ||||||
|  |     """ | ||||||
|  |     Test n°1 : Verify handling of invalid input. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         client: | ||||||
|  |         request: | ||||||
|  |     """ | ||||||
|  |     response = client.post( | ||||||
|  |         "/toilets/new", | ||||||
|  |         params={ | ||||||
|  |             "location": location, | ||||||
|  |             "radius": radius | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # checks : | ||||||
|  |     assert response.status_code == status_code | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.parametrize( | ||||||
|  |     "location,status_code", | ||||||
|  |     [ | ||||||
|  |         ([48.2270, 7.4370], 200), # Orschwiller. | ||||||
|  |         ([10.2012, 10.123], 200), # Nigerian desert. | ||||||
|  |         ([63.989, -19.677], 200), # Hekla volcano, Iceland | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  | def test_no_toilets(client, location, status_code):    # pylint: disable=redefined-outer-name | ||||||
|  |     """ | ||||||
|  |     Test n°3 : Verify the code finds some toilets in big cities. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         client: | ||||||
|  |         request: | ||||||
|  |     """ | ||||||
|  |     response = client.post( | ||||||
|  |         "/toilets/new", | ||||||
|  |         params={ | ||||||
|  |             "location": location | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  |     toilets_list = [Toilets.model_validate(toilet) for toilet in response.json()] | ||||||
|  |  | ||||||
|  |     # checks : | ||||||
|  |     assert response.status_code == 200  # check for successful planning | ||||||
|  |     assert isinstance(toilets_list, list)  # check that the return type is a list | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.parametrize( | ||||||
|  |     "location,status_code", | ||||||
|  |     [ | ||||||
|  |         ([45.7576485, 4.8330241], 200), # Lyon, Bellecour. | ||||||
|  |         ([-6.913795,  107.60278], 200), # Bandung, train station | ||||||
|  |         ([-22.970140, -43.18181], 200), # Rio de Janeiro, Copacabana | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  | def test_toilets(client, location, status_code):    # pylint: disable=redefined-outer-name | ||||||
|  |     """ | ||||||
|  |     Test n°3 : Verify the code finds some toilets in big cities. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         client: | ||||||
|  |         request: | ||||||
|  |     """ | ||||||
|  |     response = client.post( | ||||||
|  |         "/toilets/new", | ||||||
|  |         params={ | ||||||
|  |             "location": location, | ||||||
|  |             "radius" : 600 | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  |     toilets_list = [Toilets.model_validate(toilet) for toilet in response.json()] | ||||||
|  |  | ||||||
|  |     # checks : | ||||||
|  |     assert response.status_code == 200  # check for successful planning | ||||||
|  |     assert isinstance(toilets_list, list)  # check that the return type is a list | ||||||
|  |     assert len(toilets_list) > 0 | ||||||
| @@ -1,11 +1,13 @@ | |||||||
| """Helper methods for testing.""" | """Helper methods for testing.""" | ||||||
| from typing import List | 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 ..persistence import client as cache_client | ||||||
|  |  | ||||||
|  |  | ||||||
| def landmarks_to_osmid(landmarks: List[Landmark]) -> List[int] : | def landmarks_to_osmid(landmarks: list[Landmark]) -> list[int] : | ||||||
|     """ |     """ | ||||||
|     Convert the list of landmarks into a list containing their osm ids for quick landmark checking. |     Convert the list of landmarks into a list containing their osm ids for quick landmark checking. | ||||||
|      |      | ||||||
| @@ -31,22 +33,68 @@ def fetch_landmark(client, landmark_uuid: str): | |||||||
|     Returns: |     Returns: | ||||||
|         dict: Landmark data fetched from the API. |         dict: Landmark data fetched from the API. | ||||||
|     """ |     """ | ||||||
|  |     logger = logging.getLogger(__name__) | ||||||
|     response = client.get(f"/landmark/{landmark_uuid}") |     response = client.get(f"/landmark/{landmark_uuid}") | ||||||
|  |  | ||||||
|     if response.status_code != 200: |     if response.status_code != 200: | ||||||
|         raise HTTPException(status_code=999, |         raise HTTPException(status_code=500, | ||||||
|                             detail=f"Failed to fetch landmark with UUID {landmark_uuid}: {response.status_code}") |                             detail=f"Failed to fetch landmark with UUID {landmark_uuid}: {response.status_code}") | ||||||
|  |  | ||||||
|  |     try: | ||||||
|         json_data = response.json() |         json_data = response.json() | ||||||
|  |         logger.info(f"API Response: {json_data}") | ||||||
|  |     except ValueError as e: | ||||||
|  |         logger.error(f"Failed to parse response as JSON: {response.text}") | ||||||
|  |         raise HTTPException(status_code=500, detail="Invalid response format from API") | ||||||
|  |      | ||||||
|  |     # Try validating against the Landmark model here to ensure consistency | ||||||
|  |     try: | ||||||
|  |         landmark = Landmark(**json_data) | ||||||
|  |     except ValidationError as ve: | ||||||
|  |         logging.error(f"Validation error: {ve}") | ||||||
|  |         raise HTTPException(status_code=500, detail="Invalid data format received from API") | ||||||
|  |      | ||||||
|  |  | ||||||
|     if "detail" in json_data: |     if "detail" in json_data: | ||||||
|         raise HTTPException(status_code=999, detail=json_data["detail"]) |         raise HTTPException(status_code=500, detail=json_data["detail"]) | ||||||
|  |  | ||||||
|  |     return Landmark(**json_data) | ||||||
|  |  | ||||||
|  |  | ||||||
|     return json_data | def fetch_landmark_cache(landmark_uuid: str): | ||||||
|  |     """ | ||||||
|  |     Fetch landmark data from the cache based on the landmark UUID. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         landmark_uuid (str): The UUID of the landmark. | ||||||
|  |  | ||||||
|  |     Returns: | ||||||
|  |         dict: Landmark data fetched from the cache or raises an HTTP exception. | ||||||
|  |     """ | ||||||
|  |     logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |     # Try to fetch the landmark data from the cache | ||||||
|  |     try: | ||||||
|  |         landmark = cache_client.get(f"landmark_{landmark_uuid}") | ||||||
|  |         if not landmark : | ||||||
|  |             logger.warning(f"Cache miss for landmark UUID: {landmark_uuid}") | ||||||
|  |             raise HTTPException(status_code=404, detail=f"Landmark with UUID {landmark_uuid} not found in cache.") | ||||||
|  |          | ||||||
|  |         # Validate that the fetched data is a dictionary | ||||||
|  |         if not isinstance(landmark, Landmark): | ||||||
|  |             logger.error(f"Invalid cache data format for landmark UUID: {landmark_uuid}. Expected dict, got {type(landmark).__name__}.") | ||||||
|  |             raise HTTPException(status_code=500, detail="Invalid cache data format.") | ||||||
|  |  | ||||||
|  |         return landmark | ||||||
|  |      | ||||||
|  |     except Exception as exc: | ||||||
|  |         logger.error(f"Unexpected error occurred while fetching landmark UUID {landmark_uuid}: {exc}") | ||||||
|  |         raise HTTPException(status_code=500, detail="An unexpected error occurred while fetching the landmark from the cache") from exc | ||||||
|      |      | ||||||
|      |      | ||||||
| def load_trip_landmarks(client, first_uuid: str) -> List[Landmark]: |  | ||||||
|  |  | ||||||
|  | def load_trip_landmarks(client, first_uuid: str, from_cache=None) -> list[Landmark]: | ||||||
|     """ |     """ | ||||||
|     Load all landmarks for a trip using the response from the API. |     Load all landmarks for a trip using the response from the API. | ||||||
|  |  | ||||||
| @@ -60,19 +108,18 @@ def load_trip_landmarks(client, first_uuid: str) -> List[Landmark]: | |||||||
|     next_uuid = first_uuid |     next_uuid = first_uuid | ||||||
|  |  | ||||||
|     while next_uuid is not None: |     while next_uuid is not None: | ||||||
|         landmark_data = fetch_landmark(client, next_uuid) |         if from_cache : | ||||||
|         # # Convert UUIDs to strings explicitly |             landmark = fetch_landmark_cache(next_uuid) | ||||||
|         # landmark_data = { |         else : | ||||||
|         #     key: str(value) if isinstance(value, UUID) else value |             landmark = fetch_landmark(client, next_uuid) | ||||||
|         #     for key, value in landmark_data.items() |  | ||||||
|         # } |         landmarks.append(landmark) | ||||||
|         landmarks.append(Landmark(**landmark_data)) # Create Landmark objects |         next_uuid = landmark.next_uuid  # Prepare for the next iteration | ||||||
|         next_uuid = landmark_data.get('next_uuid')  # Prepare for the next iteration |  | ||||||
|  |  | ||||||
|     return landmarks |     return landmarks | ||||||
|  |  | ||||||
|  |  | ||||||
| def log_trip_details(request, landmarks: List[Landmark], duration: int, target_duration: int) : | def log_trip_details(request, landmarks: list[Landmark], duration: int, target_duration: int) : | ||||||
|     """ |     """ | ||||||
|     Allows to show the detailed trip in the html test report. |     Allows to show the detailed trip in the html test report. | ||||||
|      |      | ||||||
|   | |||||||
							
								
								
									
										283
									
								
								backend/src/utils/cluster_processing.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								backend/src/utils/cluster_processing.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,283 @@ | |||||||
|  | import logging | ||||||
|  | from typing import Literal | ||||||
|  |  | ||||||
|  | import numpy as np | ||||||
|  | from sklearn.cluster import DBSCAN | ||||||
|  | from pydantic import BaseModel | ||||||
|  | from OSMPythonTools.overpass import Overpass, overpassQueryBuilder | ||||||
|  | from OSMPythonTools.cachingStrategy import CachingStrategy, JSON | ||||||
|  |  | ||||||
|  | from ..structs.landmark import Landmark | ||||||
|  | from ..utils.get_time_separation import get_distance | ||||||
|  | from ..constants import AMENITY_SELECTORS_PATH, LANDMARK_PARAMETERS_PATH, OPTIMIZER_PARAMETERS_PATH, OSM_CACHE_DIR | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ShoppingLocation(BaseModel): | ||||||
|  |     """" | ||||||
|  |     A classe representing an interesting area for shopping. | ||||||
|  |      | ||||||
|  |     It can represent either a general area or a specifc route with start and end point. | ||||||
|  |     The importance represents the number of shops found in this cluster. | ||||||
|  |      | ||||||
|  |     Attributes: | ||||||
|  |         type :       either a 'street' or 'area' (representing a denser field of shops). | ||||||
|  |         importance : size of the cluster (number of points). | ||||||
|  |         centroid :   center of the cluster. | ||||||
|  |         start :      if the type is a street it goes from here... | ||||||
|  |         end :        ...to here | ||||||
|  |     """ | ||||||
|  |     type: Literal['street', 'area'] | ||||||
|  |     importance: int | ||||||
|  |     centroid: tuple | ||||||
|  |     # start: Optional[list] = None      # for later use if we want to have streets as well | ||||||
|  |     # end: Optional[list] = None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ShoppingManager: | ||||||
|  |  | ||||||
|  |     logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |     # NOTE: all points are in (lat, lon) format | ||||||
|  |     valid: bool             # Ensure the manager is valid (ie there are some clusters to be found)  | ||||||
|  |     all_points: list | ||||||
|  |     cluster_points: list | ||||||
|  |     cluster_labels: list | ||||||
|  |     shopping_locations: list[ShoppingLocation] | ||||||
|  |  | ||||||
|  |     def __init__(self, bbox: tuple) -> None: | ||||||
|  |         """ | ||||||
|  |         Upon intialization, generate the point cloud used for cluster detection. | ||||||
|  |         The points represent bag/clothes shops and general boutiques. | ||||||
|  |  | ||||||
|  |         Args:  | ||||||
|  |             bbox: The bounding box coordinates (around:radius, center_lat, center_lon). | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         # Initialize overpass and cache | ||||||
|  |         self.overpass = Overpass() | ||||||
|  |         CachingStrategy.use(JSON, cacheDir=OSM_CACHE_DIR) | ||||||
|  |  | ||||||
|  |         # Initialize the points for cluster detection | ||||||
|  |         query = overpassQueryBuilder( | ||||||
|  |             bbox = bbox, | ||||||
|  |             elementType = ['node'], | ||||||
|  |             selector = ['"shop"~"^(bag|boutique|clothes)$"'], | ||||||
|  |             includeCenter = True, | ||||||
|  |             out = 'skel' | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             result = self.overpass.query(query) | ||||||
|  |         except Exception as e: | ||||||
|  |             self.logger.error(f"Error fetching landmarks: {e}") | ||||||
|  |  | ||||||
|  |         if len(result.elements()) == 0 : | ||||||
|  |             self.valid = False | ||||||
|  |          | ||||||
|  |         else : | ||||||
|  |             points = [] | ||||||
|  |             for elem in result.elements() : | ||||||
|  |                 points.append(tuple((elem.lat(), elem.lon()))) | ||||||
|  |  | ||||||
|  |             self.all_points = np.array(points) | ||||||
|  |             self.valid = True             | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     def generate_shopping_landmarks(self) -> list[Landmark]: | ||||||
|  |         """ | ||||||
|  |         Generate shopping landmarks based on clustered locations. | ||||||
|  |  | ||||||
|  |         This method first generates clusters of locations and then  extracts shopping-related  | ||||||
|  |         locations from these clusters. It transforms each shopping location into a `Landmark` object. | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |             list[Landmark]: A list of `Landmark` objects representing shopping locations. | ||||||
|  |                             Returns an empty list if no clusters are found. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         self.generate_clusters() | ||||||
|  |  | ||||||
|  |         if len(set(self.cluster_labels)) == 0 : | ||||||
|  |             return []       # Return empty list if no clusters were found | ||||||
|  |  | ||||||
|  |         # Then generate the shopping locations | ||||||
|  |         self.generate_shopping_locations() | ||||||
|  |  | ||||||
|  |         # Transform the locations in landmarks and return the list | ||||||
|  |         shopping_landmarks = [] | ||||||
|  |         for location in self.shopping_locations : | ||||||
|  |             shopping_landmarks.append(self.create_landmark(location)) | ||||||
|  |  | ||||||
|  |         return shopping_landmarks | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     def generate_clusters(self) : | ||||||
|  |         """ | ||||||
|  |         Generate clusters of points using DBSCAN. | ||||||
|  |  | ||||||
|  |         This method applies the DBSCAN clustering algorithm with different | ||||||
|  |         parameters depending on the size of the city (number of points).  | ||||||
|  |         It filters out noise points and keeps only the largest clusters. | ||||||
|  |  | ||||||
|  |         The method updates: | ||||||
|  |             - `self.cluster_points`: The points belonging to clusters. | ||||||
|  |             - `self.cluster_labels`: The labels for the points in clusters. | ||||||
|  |          | ||||||
|  |         The method also calls `filter_clusters()` to retain only the largest clusters. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         # Apply DBSCAN to find clusters. Choose different settings for different cities. | ||||||
|  |         if len(self.all_points) > 200 : | ||||||
|  |             dbscan = DBSCAN(eps=0.00118, min_samples=15, algorithm='kd_tree')  # for large cities | ||||||
|  |         else : | ||||||
|  |             dbscan = DBSCAN(eps=0.00075, min_samples=10, algorithm='kd_tree')  # for small cities | ||||||
|  |  | ||||||
|  |         labels = dbscan.fit_predict(self.all_points) | ||||||
|  |  | ||||||
|  |         # Separate clustered points and noise points | ||||||
|  |         self.cluster_points = self.all_points[labels != -1] | ||||||
|  |         self.cluster_labels = labels[labels != -1] | ||||||
|  |  | ||||||
|  |         # filter the clusters to keep only the largest ones | ||||||
|  |         self.filter_clusters() | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     def generate_shopping_locations(self) : | ||||||
|  |         """ | ||||||
|  |         Generate shopping locations based on clustered points. | ||||||
|  |  | ||||||
|  |         This method iterates over the different clusters, calculates the centroid  | ||||||
|  |         (as the mean of the points within each cluster), and assigns an importance  | ||||||
|  |         based on the size of the cluster. | ||||||
|  |  | ||||||
|  |         The generated shopping locations are stored in `self.shopping_locations`  | ||||||
|  |         as a list of `ShoppingLocation` objects, each with: | ||||||
|  |             - `type`: Set to 'area'. | ||||||
|  |             - `centroid`: The calculated centroid of the cluster. | ||||||
|  |             - `importance`: The number of points in the cluster. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         locations = [] | ||||||
|  |  | ||||||
|  |         # loop through the different clusters | ||||||
|  |         for label in set(self.cluster_labels): | ||||||
|  |  | ||||||
|  |             # Extract points belonging to the current cluster | ||||||
|  |             current_cluster = self.cluster_points[self.cluster_labels == label] | ||||||
|  |              | ||||||
|  |             # Calculate the centroid as the mean of the points | ||||||
|  |             centroid = np.mean(current_cluster, axis=0) | ||||||
|  |  | ||||||
|  |             locations.append(ShoppingLocation( | ||||||
|  |                 type='area', | ||||||
|  |                 centroid=centroid, | ||||||
|  |                 importance = len(current_cluster) | ||||||
|  |             )) | ||||||
|  |  | ||||||
|  |         self.shopping_locations = locations | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     def create_landmark(self, shopping_location: ShoppingLocation) -> Landmark: | ||||||
|  |         """ | ||||||
|  |         Create a Landmark object based on the given shopping location. | ||||||
|  |  | ||||||
|  |         This method queries the Overpass API for nearby neighborhoods and shopping malls  | ||||||
|  |         within a 1000m radius around the shopping location centroid. It selects the closest  | ||||||
|  |         result and creates a landmark with the associated details such as name, type, and OSM ID. | ||||||
|  |  | ||||||
|  |         Parameters: | ||||||
|  |             shopping_location (ShoppingLocation): A ShoppingLocation object containing  | ||||||
|  |             the centroid and importance of the area. | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |             Landmark: A Landmark object containing details such as the name, type,  | ||||||
|  |             location, attractiveness, and OSM details. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         # Define the bounding box for a given radius around the coordinates | ||||||
|  |         lat, lon = shopping_location.centroid | ||||||
|  |         bbox = ("around:1000", str(lat), str(lon)) | ||||||
|  |  | ||||||
|  |         # Query neighborhoods and shopping malls | ||||||
|  |         selectors = ['"place"~"^(suburb|neighborhood|neighbourhood|quarter|city_block)$"', '"shop"="mall"'] | ||||||
|  |  | ||||||
|  |         min_dist = float('inf') | ||||||
|  |         new_name = 'Shopping Area' | ||||||
|  |         new_name_en = None | ||||||
|  |         osm_id = 0 | ||||||
|  |         osm_type = 'node' | ||||||
|  |  | ||||||
|  |         for sel in selectors :  | ||||||
|  |             query = overpassQueryBuilder( | ||||||
|  |                 bbox = bbox, | ||||||
|  |                 elementType = ['node', 'way', 'relation'], | ||||||
|  |                 selector = sel, | ||||||
|  |                 includeCenter = True, | ||||||
|  |                 out = 'center' | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             try: | ||||||
|  |                 result = self.overpass.query(query) | ||||||
|  |             except Exception as e: | ||||||
|  |                 self.logger.error(f"Error fetching landmarks: {e}") | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |             for elem in result.elements(): | ||||||
|  |                 location = (elem.centerLat(), elem.centerLon()) | ||||||
|  |  | ||||||
|  |                 if location[0] is None :  | ||||||
|  |                     location = (elem.lat(), elem.lon()) | ||||||
|  |                     if location[0] is None :  | ||||||
|  |                         continue | ||||||
|  |  | ||||||
|  |                 d = get_distance(shopping_location.centroid, location) | ||||||
|  |                 if  d < min_dist : | ||||||
|  |                     min_dist = d | ||||||
|  |                     new_name = elem.tag('name') | ||||||
|  |                     osm_type = elem.type()      # Add type: 'way' or 'relation' | ||||||
|  |                     osm_id = elem.id()          # Add OSM id  | ||||||
|  |  | ||||||
|  |                     # Add english name if it exists | ||||||
|  |                     try : | ||||||
|  |                         new_name_en = elem.tag('name:en') | ||||||
|  |                     except: | ||||||
|  |                         pass  | ||||||
|  |          | ||||||
|  |         return Landmark( | ||||||
|  |             name=new_name, | ||||||
|  |             type='shopping', | ||||||
|  |             location=shopping_location.centroid,              # TODO: use the fact the we can also recognize streets. | ||||||
|  |             attractiveness=shopping_location.importance, | ||||||
|  |             n_tags=0, | ||||||
|  |             osm_id=osm_id, | ||||||
|  |             osm_type=osm_type, | ||||||
|  |             name_en=new_name_en | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     def filter_clusters(self): | ||||||
|  |         """ | ||||||
|  |         Filter clusters to retain only the 5 largest clusters by point count. | ||||||
|  |  | ||||||
|  |         This method calculates the size of each cluster and filters out all but the  | ||||||
|  |         5 largest clusters. It then updates the cluster points and labels to reflect  | ||||||
|  |         only those from the top 5 clusters. | ||||||
|  |         """ | ||||||
|  |         label_counts = np.bincount(self.cluster_labels) | ||||||
|  |  | ||||||
|  |         # Step 3: Get the indices (labels) of the 5 largest clusters | ||||||
|  |         top_5_labels = np.argsort(label_counts)[-5:]  # Get the largest 5 clusters | ||||||
|  |  | ||||||
|  |         # Step 4: Filter points to keep only the points in the top 5 clusters | ||||||
|  |         filtered_cluster_points = [] | ||||||
|  |         filtered_cluster_labels = [] | ||||||
|  |  | ||||||
|  |         for label in top_5_labels: | ||||||
|  |             filtered_cluster_points.append(self.cluster_points[self.cluster_labels == label]) | ||||||
|  |             filtered_cluster_labels.append(np.full((label_counts[label],), label))  # Replicate the label | ||||||
|  |  | ||||||
|  |         # update the cluster points and labels with the filtered data | ||||||
|  |         self.cluster_points = np.vstack(filtered_cluster_points) | ||||||
|  |         self.cluster_labels = np.concatenate(filtered_cluster_labels) | ||||||
|  |  | ||||||
| @@ -15,8 +15,8 @@ def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int: | |||||||
|     Calculate the time in minutes to travel from one location to another. |     Calculate the time in minutes to travel from one location to another. | ||||||
|  |  | ||||||
|     Args: |     Args: | ||||||
|         p1 (Tuple[float, float]): Coordinates of the starting location. |         p1 (tuple[float, float]): Coordinates of the starting location. | ||||||
|         p2 (Tuple[float, float]): Coordinates of the destination. |         p2 (tuple[float, float]): Coordinates of the destination. | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|         int: Time to travel from p1 to p2 in minutes. |         int: Time to travel from p1 to p2 in minutes. | ||||||
| @@ -48,3 +48,35 @@ def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int: | |||||||
|     walk_time = walk_distance / AVERAGE_WALKING_SPEED * 60 |     walk_time = walk_distance / AVERAGE_WALKING_SPEED * 60 | ||||||
|  |  | ||||||
|     return round(walk_time) |     return round(walk_time) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_distance(p1: tuple[float, float], p2: tuple[float, float]) -> int: | ||||||
|  |     """ | ||||||
|  |     Calculate the time in minutes to travel from one location to another. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         p1 (tuple[float, float]): Coordinates of the starting location. | ||||||
|  |         p2 (tuple[float, float]): Coordinates of the destination. | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |         int: Time to travel from p1 to p2 in minutes. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     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]) | ||||||
|  |  | ||||||
|  |         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)) | ||||||
|  |  | ||||||
|  |         return EARTH_RADIUS_KM * c | ||||||
| @@ -1,13 +1,11 @@ | |||||||
| import math | import math, yaml, logging | ||||||
| import yaml |  | ||||||
| import logging |  | ||||||
|  |  | ||||||
| from OSMPythonTools.overpass import Overpass, overpassQueryBuilder | from OSMPythonTools.overpass import Overpass, overpassQueryBuilder | ||||||
| from OSMPythonTools.cachingStrategy import CachingStrategy, JSON | from OSMPythonTools.cachingStrategy import CachingStrategy, JSON | ||||||
|  |  | ||||||
| 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 .take_most_important import take_most_important | ||||||
|  | from .cluster_processing import ShoppingManager | ||||||
|  |  | ||||||
| from ..constants import AMENITY_SELECTORS_PATH, LANDMARK_PARAMETERS_PATH, OPTIMIZER_PARAMETERS_PATH, OSM_CACHE_DIR | from ..constants import AMENITY_SELECTORS_PATH, LANDMARK_PARAMETERS_PATH, OPTIMIZER_PARAMETERS_PATH, OSM_CACHE_DIR | ||||||
|  |  | ||||||
| @@ -79,7 +77,9 @@ class LandmarkManager: | |||||||
|         # use set to avoid duplicates, this requires some __methods__ to be set in Landmark |         # use set to avoid duplicates, this requires some __methods__ to be set in Landmark | ||||||
|         all_landmarks = set() |         all_landmarks = set() | ||||||
|  |  | ||||||
|         bbox = self.create_bbox(center_coordinates, reachable_bbox_side) |         # Create a bbox using the around technique | ||||||
|  |         bbox = tuple((f"around:{reachable_bbox_side/2}", str(center_coordinates[0]), str(center_coordinates[1]))) | ||||||
|  |          | ||||||
|         # list for sightseeing |         # list for sightseeing | ||||||
|         if preferences.sightseeing.score != 0: |         if preferences.sightseeing.score != 0: | ||||||
|             score_function = lambda score: score * 10 * preferences.sightseeing.score / 5 |             score_function = lambda score: score * 10 * preferences.sightseeing.score / 5 | ||||||
| @@ -96,10 +96,19 @@ class LandmarkManager: | |||||||
|         if preferences.shopping.score != 0: |         if preferences.shopping.score != 0: | ||||||
|             score_function = lambda score: score * 10 * preferences.shopping.score / 5 |             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) |             current_landmarks = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function) | ||||||
|  |              | ||||||
|             # set time for all shopping activites : |             # set time for all shopping activites : | ||||||
|             for landmark in current_landmarks : landmark.duration = 45 |             for landmark in current_landmarks : landmark.duration = 30 | ||||||
|             all_landmarks.update(current_landmarks) |             all_landmarks.update(current_landmarks) | ||||||
|  |  | ||||||
|  |             # special pipeline for shopping malls | ||||||
|  |             shopping_manager = ShoppingManager(bbox) | ||||||
|  |             if shopping_manager.valid : | ||||||
|  |                 shopping_clusters = shopping_manager.generate_shopping_landmarks() | ||||||
|  |                 for landmark in shopping_clusters : landmark.duration = 45 | ||||||
|  |                 all_landmarks.update(shopping_clusters) | ||||||
|  |              | ||||||
|  |  | ||||||
|  |  | ||||||
|         landmarks_constrained = take_most_important(all_landmarks, self.N_important) |         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.') |         self.logger.info(f'Generated {len(all_landmarks)} landmarks around {center_coordinates}, and constrained to {len(landmarks_constrained)} most important ones.') | ||||||
| @@ -151,36 +160,24 @@ class LandmarkManager: | |||||||
|             return 0 |             return 0 | ||||||
|  |  | ||||||
|  |  | ||||||
|     def create_bbox(self, coordinates: tuple[float, float], reachable_bbox_side: int) -> tuple[float, float, float, float]: |     # def create_bbox(self, coordinates: tuple[float, float], reachable_bbox_side: int) -> tuple[float, float, float, float]: | ||||||
|         """ |     #     """ | ||||||
|         Create a bounding box around the given coordinates. |     #     Create a bounding box around the given coordinates. | ||||||
|  |  | ||||||
|         Args: |     #     Args: | ||||||
|             coordinates (tuple[float, float]): The latitude and longitude of the center of the bounding box. |     #         coordinates (tuple[float, float]): The latitude and longitude of the center of the bounding box. | ||||||
|             reachable_bbox_side (int): The side length of the bounding box in meters. |     #         reachable_bbox_side (int): The side length of the bounding box in meters. | ||||||
|  |  | ||||||
|         Returns: |     #     Returns: | ||||||
|             tuple[float, float, float, float]: The minimum latitude, minimum longitude, maximum latitude, and maximum longitude |     #         tuple[float, float, float, float]: The minimum latitude, minimum longitude, maximum latitude, and maximum longitude | ||||||
|                                                 defining the bounding box. |     #                                             defining the bounding box. | ||||||
|         """ |     #     """ | ||||||
|  |  | ||||||
|         lat = coordinates[0] |     #     # Half the side length in m (since it's a square bbox) | ||||||
|         lon = coordinates[1] |     #     half_side_length_m = reachable_bbox_side / 2 | ||||||
|  |  | ||||||
|         # Half the side length in km (since it's a square bbox) |     #     return tuple((f"around:{half_side_length_m}", str(coordinates[0]), str(coordinates[1]))) | ||||||
|         half_side_length_km = reachable_bbox_side / 2 / 1000 |  | ||||||
|  |  | ||||||
|         # 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 |  | ||||||
|  |  | ||||||
|         # Calculate bbox |  | ||||||
|         min_lat = lat - lat_diff |  | ||||||
|         max_lat = lat + lat_diff |  | ||||||
|         min_lon = lon - lon_diff |  | ||||||
|         max_lon = lon + lon_diff |  | ||||||
|  |  | ||||||
|         return min_lat, min_lon, max_lat, max_lon |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     def fetch_landmarks(self, bbox: tuple, amenity_selector: dict, landmarktype: str, score_function: callable) -> list[Landmark]: |     def fetch_landmarks(self, bbox: tuple, amenity_selector: dict, landmarktype: str, score_function: callable) -> list[Landmark]: | ||||||
| @@ -188,7 +185,7 @@ class LandmarkManager: | |||||||
|         Fetches landmarks of a specified type from OpenStreetMap (OSM) within a bounding box centered on given coordinates. |         Fetches landmarks of a specified type from OpenStreetMap (OSM) within a bounding box centered on given coordinates. | ||||||
|  |  | ||||||
|         Args: |         Args: | ||||||
|             bbox (tuple[float, float, float, float]): The bounding box coordinates (min_lat, min_lon, max_lat, max_lon). |             bbox (tuple[float, float, float, float]): The bounding box coordinates (around:radius, center_lat, center_lon). | ||||||
|             amenity_selector (dict): The Overpass API query selector for the desired landmark type.  |             amenity_selector (dict): The Overpass API query selector for the desired landmark type.  | ||||||
|             landmarktype (str): The type of the landmark (e.g., 'sightseeing', 'nature', 'shopping'). |             landmarktype (str): The type of the landmark (e.g., 'sightseeing', 'nature', 'shopping'). | ||||||
|             score_function (callable): The function to compute the score of the landmark based on its attributes. |             score_function (callable): The function to compute the score of the landmark based on its attributes. | ||||||
| @@ -212,7 +209,9 @@ class LandmarkManager: | |||||||
|         for sel in dict_to_selector_list(amenity_selector): |         for sel in dict_to_selector_list(amenity_selector): | ||||||
|             self.logger.debug(f"Current selector: {sel}") |             self.logger.debug(f"Current selector: {sel}") | ||||||
|  |  | ||||||
|             query_conditions = ['count_tags()>5'] |             # query_conditions = ['count_tags()>5'] | ||||||
|  |             # if landmarktype == 'shopping' :       # use this later for shopping clusters | ||||||
|  |             #     element_types = ['node'] | ||||||
|             element_types = ['way', 'relation'] |             element_types = ['way', 'relation'] | ||||||
|  |  | ||||||
|             if 'viewpoint' in sel : |             if 'viewpoint' in sel : | ||||||
| @@ -228,7 +227,7 @@ class LandmarkManager: | |||||||
|                 selector = sel, |                 selector = sel, | ||||||
|                 conditions = query_conditions,        # except for nature.... |                 conditions = query_conditions,        # except for nature.... | ||||||
|                 includeCenter = True, |                 includeCenter = True, | ||||||
|                 out = 'body' |                 out = 'center' | ||||||
|                 ) |                 ) | ||||||
|             self.logger.debug(f"Query: {query}") |             self.logger.debug(f"Query: {query}") | ||||||
|  |  | ||||||
| @@ -365,7 +364,6 @@ class LandmarkManager: | |||||||
|         return return_list |         return return_list | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| 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. | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ class Optimizer: | |||||||
|             resx (list[float]): List of edge weights. |             resx (list[float]): List of edge weights. | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             Tuple[list[int], list[int]]: A tuple containing a new row for constraint matrix and new value for upper bound vector. |             tuple[list[int], list[int]]: A tuple containing a new row for constraint matrix and new value for upper bound vector. | ||||||
|         """ |         """ | ||||||
|          |          | ||||||
|         for i, elem in enumerate(resx): |         for i, elem in enumerate(resx): | ||||||
| @@ -79,7 +79,7 @@ class Optimizer: | |||||||
|             L (int): Number of landmarks. |             L (int): Number of landmarks. | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             Tuple[np.ndarray, list[int]]: A tuple containing a new row for constraint matrix and new value for upper bound vector. |             tuple[np.ndarray, list[int]]: A tuple containing a new row for constraint matrix and new value for upper bound vector. | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         l1 = [0]*L*L |         l1 = [0]*L*L | ||||||
| @@ -107,7 +107,7 @@ class Optimizer: | |||||||
|             resx (list): List of edge weights. |             resx (list): List of edge weights. | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             Tuple[list[int], Optional[list[list[int]]]]: A tuple containing the visit order and a list of any detected circles. |             tuple[list[int], Optional[list[list[int]]]]: A tuple containing the visit order and a list of any detected circles. | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         # first round the results to have only 0-1 values |         # first round the results to have only 0-1 values | ||||||
| @@ -180,7 +180,7 @@ class Optimizer: | |||||||
|             max_time (int): Maximum time of visit allowed. |             max_time (int): Maximum time of visit allowed. | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             Tuple[list[float], list[float], list[int]]: Objective function coefficients, inequality constraint coefficients, and the right-hand side of the inequality constraint. |             tuple[list[float], list[float], list[int]]: Objective function coefficients, inequality constraint coefficients, and the right-hand side of the inequality constraint. | ||||||
|         """ |         """ | ||||||
|          |          | ||||||
|         # Objective function coefficients. a*x1 + b*x2 + c*x3 + ... |         # Objective function coefficients. a*x1 + b*x2 + c*x3 + ... | ||||||
| @@ -212,7 +212,7 @@ class Optimizer: | |||||||
|             L (int): Number of landmarks. |             L (int): Number of landmarks. | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. |             tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         ones = [1]*L |         ones = [1]*L | ||||||
| @@ -239,7 +239,7 @@ class Optimizer: | |||||||
|             L (int): Number of landmarks. |             L (int): Number of landmarks. | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. |             tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         upper_ind = np.triu_indices(L,0,L) |         upper_ind = np.triu_indices(L,0,L) | ||||||
| @@ -270,7 +270,7 @@ class Optimizer: | |||||||
|             L (int): Number of landmarks. |             L (int): Number of landmarks. | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             Tuple[list[np.ndarray], list[int]]: Equality constraint coefficients and the right-hand side of the equality constraints. |             tuple[list[np.ndarray], list[int]]: Equality constraint coefficients and the right-hand side of the equality constraints. | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         l = [0]*L*L |         l = [0]*L*L | ||||||
| @@ -293,7 +293,7 @@ class Optimizer: | |||||||
|             landmarks (list[Landmark]): List of landmarks, where some are marked as 'must_do'. |             landmarks (list[Landmark]): List of landmarks, where some are marked as 'must_do'. | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. |             tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         L = len(landmarks) |         L = len(landmarks) | ||||||
| @@ -319,7 +319,7 @@ class Optimizer: | |||||||
|             landmarks (list[Landmark]): List of landmarks, where some are marked as 'must_avoid'. |             landmarks (list[Landmark]): List of landmarks, where some are marked as 'must_avoid'. | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. |             tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         L = len(landmarks) |         L = len(landmarks) | ||||||
| @@ -346,7 +346,7 @@ class Optimizer: | |||||||
|             L (int): Number of landmarks. |             L (int): Number of landmarks. | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. |             tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         l_start = [1]*L + [0]*L*(L-1)   # sets departures only for start (horizontal ones) |         l_start = [1]*L + [0]*L*(L-1)   # sets departures only for start (horizontal ones) | ||||||
| @@ -374,7 +374,7 @@ class Optimizer: | |||||||
|             L (int): Number of landmarks. |             L (int): Number of landmarks. | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             Tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. |             tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         A = [0]*L*L |         A = [0]*L*L | ||||||
|   | |||||||
| @@ -2,7 +2,6 @@ import yaml, logging | |||||||
|  |  | ||||||
| from shapely import buffer, LineString, Point, Polygon, MultiPoint, concave_hull | from shapely import buffer, LineString, Point, Polygon, MultiPoint, concave_hull | ||||||
| from math import pi | from math import pi | ||||||
| from typing import List |  | ||||||
|  |  | ||||||
| from ..structs.landmark import Landmark | from ..structs.landmark import Landmark | ||||||
| from . import take_most_important, get_time_separation | from . import take_most_important, get_time_separation | ||||||
| @@ -135,7 +134,7 @@ class Refiner : | |||||||
|      |      | ||||||
|         return tour |         return tour | ||||||
|      |      | ||||||
|     def integrate_landmarks(self, sub_list: List[Landmark], main_list: List[Landmark]) : |     def integrate_landmarks(self, sub_list: list[Landmark], main_list: list[Landmark]) : | ||||||
|         """ |         """ | ||||||
|         Inserts 'sub_list' of Landmarks inside the 'main_list' by leaving the ends untouched. |         Inserts 'sub_list' of Landmarks inside the 'main_list' by leaving the ends untouched. | ||||||
|          |          | ||||||
|   | |||||||
							
								
								
									
										78
									
								
								backend/src/utils/toilets_manager.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								backend/src/utils/toilets_manager.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | |||||||
|  | import logging, yaml | ||||||
|  | from OSMPythonTools.overpass import Overpass, overpassQueryBuilder | ||||||
|  | from OSMPythonTools.cachingStrategy import CachingStrategy, JSON | ||||||
|  |  | ||||||
|  | from ..structs.landmark import Toilets | ||||||
|  | from ..constants import LANDMARK_PARAMETERS_PATH, OSM_CACHE_DIR | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # silence the overpass logger | ||||||
|  | logging.getLogger('OSMPythonTools').setLevel(level=logging.CRITICAL) | ||||||
|  |  | ||||||
|  | class ToiletsManager: | ||||||
|  |  | ||||||
|  |     logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |     location: tuple[float, float] | ||||||
|  |     radius: int    # radius in meters | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     def __init__(self, location: tuple[float, float], radius : int) -> None: | ||||||
|  |  | ||||||
|  |         self.radius = radius | ||||||
|  |         self.location = location | ||||||
|  |         self.overpass = Overpass() | ||||||
|  |         CachingStrategy.use(JSON, cacheDir=OSM_CACHE_DIR) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     def generate_toilet_list(self) -> list[Toilets] : | ||||||
|  |  | ||||||
|  |          | ||||||
|  |         # Create a bbox using the around technique | ||||||
|  |         bbox = tuple((f"around:{self.radius}", str(self.location[0]), str(self.location[1]))) | ||||||
|  |         toilets_list = [] | ||||||
|  |  | ||||||
|  |         query = overpassQueryBuilder( | ||||||
|  |                 bbox = bbox, | ||||||
|  |                 elementType = ['node', 'way', 'relation'], | ||||||
|  |                 # selector can in principle be a list already, | ||||||
|  |                 # but it generates the intersection of the queries | ||||||
|  |                 # we want the union | ||||||
|  |                 selector = ['"amenity"="toilets"'], | ||||||
|  |                 includeCenter = True, | ||||||
|  |                 out = 'center' | ||||||
|  |                 ) | ||||||
|  |         self.logger.debug(f"Query: {query}") | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             result = self.overpass.query(query) | ||||||
|  |         except Exception as e: | ||||||
|  |             self.logger.error(f"Error fetching landmarks: {e}") | ||||||
|  |             return None | ||||||
|  |  | ||||||
|  |         for elem in result.elements(): | ||||||
|  |             location = (elem.centerLat(), elem.centerLon()) | ||||||
|  |  | ||||||
|  |             # handle unprecise and no-name locations | ||||||
|  |             if location[0] is None: | ||||||
|  |                     location = (elem.lat(), elem.lon()) | ||||||
|  |             else :  | ||||||
|  |                 continue | ||||||
|  |              | ||||||
|  |             toilets = Toilets(location=location) | ||||||
|  |              | ||||||
|  |             if 'wheelchair' in elem.tags().keys() and elem.tag('wheelchair') == 'yes': | ||||||
|  |                 toilets.wheelchair = True | ||||||
|  |  | ||||||
|  |             if 'changing_table' in elem.tags().keys() and elem.tag('changing_table') == 'yes': | ||||||
|  |                 toilets.changing_table = True | ||||||
|  |  | ||||||
|  |             if 'fee' in elem.tags().keys() and elem.tag('fee') == 'yes': | ||||||
|  |                 toilets.fee = True | ||||||
|  |  | ||||||
|  |             if 'opening_hours' in elem.tags().keys() : | ||||||
|  |                 toilets.opening_hours = elem.tag('opening_hours') | ||||||
|  |  | ||||||
|  |             toilets_list.append(toilets) | ||||||
|  |  | ||||||
|  |         return toilets_list | ||||||
		Reference in New Issue
	
	Block a user