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.")
|
||||||
@ -69,7 +66,7 @@ def new_trip(preferences: Preferences,
|
|||||||
osm_id=0,
|
osm_id=0,
|
||||||
attractiveness=0,
|
attractiveness=0,
|
||||||
must_do=True,
|
must_do=True,
|
||||||
n_tags = 0)
|
n_tags=0)
|
||||||
|
|
||||||
# Generate the landmarks from the start location
|
# Generate the landmarks from the start location
|
||||||
landmarks, landmarks_short = manager.generate_landmarks_list(
|
landmarks, landmarks_short = manager.generate_landmarks_list(
|
||||||
@ -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}")
|
||||||
|
|
||||||
json_data = response.json()
|
try:
|
||||||
|
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
|
Loading…
x
Reference in New Issue
Block a user