Merge branch 'feature/backend/unify-api-communication'

This commit is contained in:
Helldragon67 2024-06-26 11:05:57 +02:00
commit c26d9222bd
30 changed files with 11845 additions and 646 deletions

View File

@ -18,7 +18,7 @@ jobs:
with:
registry: git.kluster.moll.re
username: ${{ gitea.repository_owner }}
password: ${{ secrets.GITEA_TOKEN}}
password: ${{ secrets.DOCKER_PUSH_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
cache/

View File

@ -8,5 +8,7 @@ numpy = "*"
scipy = "*"
fastapi = "*"
osmpythontools = "*"
pydantic = "*"
shapely = "*"
[dev-packages]

518
backend/Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "15cc819367b908ccf7d61e3f2b45574c248556600dfeaa795e19045d6781fe90"
"sha256": "0f88c01cde3be9a6332acec33fa0ccf13b6e122a6df8ee5cfefa52ba1e98034f"
},
"pipfile-spec": 6,
"requires": {},
@ -40,11 +40,11 @@
},
"certifi": {
"hashes": [
"sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f",
"sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"
"sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516",
"sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"
],
"markers": "python_version >= '3.6'",
"version": "==2024.2.2"
"version": "==2024.6.2"
},
"click": {
"hashes": [
@ -122,11 +122,11 @@
},
"email-validator": {
"hashes": [
"sha256:200a70680ba08904be6d1eef729205cc0d687634399a5924d842533efb824b84",
"sha256:97d882d174e2a65732fb43bfce81a3a834cbc1bde8bf419e30ef5ea976370a05"
"sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631",
"sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"
],
"markers": "python_version >= '3.8'",
"version": "==2.1.1"
"version": "==2.2.0"
},
"exceptiongroup": {
"hashes": [
@ -155,51 +155,51 @@
},
"fonttools": {
"hashes": [
"sha256:00d9abf4b400f98fb895566eb298f60432b4b29048e3dc02807427b09a06604e",
"sha256:05e4291db6af66f466a203d9922e4c1d3e18ef16868f76f10b00e2c3b9814df2",
"sha256:15efb2ba4b8c2d012ee0bb7a850c2e4780c530cc83ec8e843b2a97f8b3a5fd4b",
"sha256:1dc626de4b204d025d029e646bae8fdbf5acd9217158283a567f4b523fda3bae",
"sha256:21921e5855c399d10ddfc373538b425cabcf8b3258720b51450909e108896450",
"sha256:309b617942041073ffa96090d320b99d75648ed16e0c67fb1aa7788e06c834de",
"sha256:346d08ff92e577b2dc5a0c228487667d23fe2da35a8b9a8bba22c2b6ba8be21c",
"sha256:35af630404223273f1d7acd4761f399131c62820366f53eac029337069f5826a",
"sha256:46cc5d06ee05fd239c45d7935aaffd060ee773a88b97e901df50478247472643",
"sha256:4b0b9eb0f55dce9c7278ad4175f1cbaed23b799dce5ecc20e3213da241584140",
"sha256:4b419207e53db1599b3d385afd4bca6692c219d53732890d0814a2593104d0e2",
"sha256:4c3ad89204c2d7f419436f1d6fde681b070c5e20b888beb57ccf92f640628cc9",
"sha256:52f6001814ec5e0c961cabe89642f7e8d7e07892b565057aa526569b9ebb711c",
"sha256:5ecb88318ff249bd2a715e7aec36774ce7ae3441128007ef72a39a60601f4a8f",
"sha256:70d87f2099006304d33438bdaa5101953b7e22e23a93b1c7b7ed0f32ff44b423",
"sha256:73ba38b98c012957940a04d9eb5439b42565ac892bba8cfc32e10d88e73921fe",
"sha256:7467161f1eed557dbcec152d5ee95540200b1935709fa73307da16bc0b7ca361",
"sha256:7dccf4666f716e5e0753f0fa28dad2f4431154c87747bc781c838b8a5dca990e",
"sha256:859399b7adc8ac067be8e5c80ef4bb2faddff97e9b40896a9de75606a43d0469",
"sha256:8873d6edd1dae5c088dd3d61c9fd4dd80c827c486fa224d368233e7f33dc98af",
"sha256:890e7a657574610330e42dd1e38d3b9e0a8cb0eff3da080f80995460a256d3dd",
"sha256:89b53386214197bd5b3e3c753895bad691de84726ced3c222a59cde1dd12d57b",
"sha256:8b186cd6b8844f6cf04a7e0a174bc3649d3deddbfc10dc59846a4381f796d348",
"sha256:9180775c9535389a665cae7c5282f8e07754beabf59b66aeba7f6bfeb32a3652",
"sha256:95e8a5975d08d0b624a14eec0f987e204ad81b480e24c5436af99170054434b8",
"sha256:9725687db3c1cef13c0f40b380c3c15bea0113f4d0231b204d58edd5f2a53d90",
"sha256:9a5d1b0475050056d2e3bc378014f2ea2230e8ae434eeac8dfb182aa8efaf642",
"sha256:9ed23a03b7d9f0e29ca0679eafe5152aeccb0580312a3fc36f0662e178b4791b",
"sha256:a4daf2751a98c69d9620717826ed6c5743b662ef0ae7bb33dc6c205425e48eba",
"sha256:a64e72d2c144630e017ac9c1c416ddf8ac43bef9a083bf81fe08c0695f0baa95",
"sha256:a791f002d1b717268235cfae7e4957b7fd132e92e2c5400e521bf191f1b3a9a5",
"sha256:b4cba644e2515d685d4ee3ca2fbb5d53930a0e9ec2cf332ed704dc341b145878",
"sha256:b9a22cf1adaae7b2ba2ed7d8651a4193a4f348744925b4b740e6b38a94599c5b",
"sha256:bb7d206fa5ba6e082ba5d5e1b7107731029fc3a55c71c48de65121710d817986",
"sha256:cf694159528022daa71b1777cb6ec9e0ebbdd29859f3e9c845826cafaef4ca29",
"sha256:d0184aa88865339d96f7f452e8c5b621186ef7638744d78bf9b775d67e206819",
"sha256:d272c7e173c3085308345ccc7fb2ad6ce7f415d777791dd6ce4e8140e354d09c",
"sha256:d2cc7906bc0afdd2689aaf88b910307333b1f936262d1d98f25dbf8a5eb2e829",
"sha256:e03dae26084bb3632b4a77b1cd0419159d2226911aff6dc4c7e3058df68648c6",
"sha256:e176249292eccd89f81d39f514f2b5e8c75dfc9cef8653bdc3021d06697e9eff",
"sha256:ebb183ed8b789cece0bd6363121913fb6da4034af89a2fa5408e42a1592889a8",
"sha256:fb8cd6559f0ae3a8f5e146f80ab2a90ad0325a759be8d48ee82758a0b89fa0aa"
"sha256:099634631b9dd271d4a835d2b2a9e042ccc94ecdf7e2dd9f7f34f7daf333358d",
"sha256:0c555e039d268445172b909b1b6bdcba42ada1cf4a60e367d68702e3f87e5f64",
"sha256:1e677bfb2b4bd0e5e99e0f7283e65e47a9814b0486cb64a41adf9ef110e078f2",
"sha256:2367d47816cc9783a28645bc1dac07f8ffc93e0f015e8c9fc674a5b76a6da6e4",
"sha256:28d072169fe8275fb1a0d35e3233f6df36a7e8474e56cb790a7258ad822b6fd6",
"sha256:31f0e3147375002aae30696dd1dc596636abbd22fca09d2e730ecde0baad1d6b",
"sha256:3e0ad3c6ea4bd6a289d958a1eb922767233f00982cf0fe42b177657c86c80a8f",
"sha256:45b4afb069039f0366a43a5d454bc54eea942bfb66b3fc3e9a2c07ef4d617380",
"sha256:4a2a6ba400d386e904fd05db81f73bee0008af37799a7586deaa4aef8cd5971e",
"sha256:4f520d9ac5b938e6494f58a25c77564beca7d0199ecf726e1bd3d56872c59749",
"sha256:52a6e0a7a0bf611c19bc8ec8f7592bdae79c8296c70eb05917fd831354699b20",
"sha256:5a4788036201c908079e89ae3f5399b33bf45b9ea4514913f4dbbe4fac08efe0",
"sha256:6b4f04b1fbc01a3569d63359f2227c89ab294550de277fd09d8fca6185669fa4",
"sha256:715b41c3e231f7334cbe79dfc698213dcb7211520ec7a3bc2ba20c8515e8a3b5",
"sha256:73121a9b7ff93ada888aaee3985a88495489cc027894458cb1a736660bdfb206",
"sha256:74ae2441731a05b44d5988d3ac2cf784d3ee0a535dbed257cbfff4be8bb49eb9",
"sha256:7d6166192dcd925c78a91d599b48960e0a46fe565391c79fe6de481ac44d20ac",
"sha256:7f193f060391a455920d61684a70017ef5284ccbe6023bb056e15e5ac3de11d1",
"sha256:907fa0b662dd8fc1d7c661b90782ce81afb510fc4b7aa6ae7304d6c094b27bce",
"sha256:93156dd7f90ae0a1b0e8871032a07ef3178f553f0c70c386025a808f3a63b1f4",
"sha256:93bc9e5aaa06ff928d751dc6be889ff3e7d2aa393ab873bc7f6396a99f6fbb12",
"sha256:95db0c6581a54b47c30860d013977b8a14febc206c8b5ff562f9fe32738a8aca",
"sha256:973d030180eca8255b1bce6ffc09ef38a05dcec0e8320cc9b7bcaa65346f341d",
"sha256:9cd7a6beec6495d1dffb1033d50a3f82dfece23e9eb3c20cd3c2444d27514068",
"sha256:9fe9096a60113e1d755e9e6bda15ef7e03391ee0554d22829aa506cdf946f796",
"sha256:a209d2e624ba492df4f3bfad5996d1f76f03069c6133c60cd04f9a9e715595ec",
"sha256:a239afa1126b6a619130909c8404070e2b473dd2b7fc4aacacd2e763f8597fea",
"sha256:ba9f09ff17f947392a855e3455a846f9855f6cf6bec33e9a427d3c1d254c712f",
"sha256:bb7273789f69b565d88e97e9e1da602b4ee7ba733caf35a6c2affd4334d4f005",
"sha256:bd5bc124fae781a4422f61b98d1d7faa47985f663a64770b78f13d2c072410c2",
"sha256:bff98816cb144fb7b85e4b5ba3888a33b56ecef075b0e95b95bcd0a5fbf20f06",
"sha256:c4ee5a24e281fbd8261c6ab29faa7fd9a87a12e8c0eed485b705236c65999109",
"sha256:c93ed66d32de1559b6fc348838c7572d5c0ac1e4a258e76763a5caddd8944002",
"sha256:d1a24f51a3305362b94681120c508758a88f207fa0a681c16b5a4172e9e6c7a9",
"sha256:d8f191a17369bd53a5557a5ee4bab91d5330ca3aefcdf17fab9a497b0e7cff7a",
"sha256:daaef7390e632283051e3cf3e16aff2b68b247e99aea916f64e578c0449c9c68",
"sha256:e40013572bfb843d6794a3ce076c29ef4efd15937ab833f520117f8eccc84fd6",
"sha256:eceef49f457253000e6a2d0f7bd08ff4e9fe96ec4ffce2dbcb32e34d9c1b8161",
"sha256:ee595d7ba9bba130b2bec555a40aafa60c26ce68ed0cf509983e0f12d88674fd",
"sha256:ef50ec31649fbc3acf6afd261ed89d09eb909b97cc289d80476166df8438524d",
"sha256:fa1f3e34373aa16045484b4d9d352d4c6b5f9f77ac77a178252ccbc851e8b2ee",
"sha256:fca66d9ff2ac89b03f5aa17e0b21a97c21f3491c46b583bb131eb32c7bab33af"
],
"markers": "python_version >= '3.8'",
"version": "==4.52.4"
"version": "==4.53.0"
},
"geojson": {
"hashes": [
@ -667,98 +667,107 @@
},
"numpy": {
"hashes": [
"sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b",
"sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818",
"sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20",
"sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0",
"sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010",
"sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a",
"sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea",
"sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c",
"sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71",
"sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110",
"sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be",
"sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a",
"sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a",
"sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5",
"sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed",
"sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd",
"sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c",
"sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e",
"sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0",
"sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c",
"sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a",
"sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b",
"sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0",
"sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6",
"sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2",
"sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a",
"sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30",
"sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218",
"sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5",
"sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07",
"sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2",
"sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4",
"sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764",
"sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef",
"sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3",
"sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"
"sha256:04494f6ec467ccb5369d1808570ae55f6ed9b5809d7f035059000a37b8d7e86f",
"sha256:0a43f0974d501842866cc83471bdb0116ba0dffdbaac33ec05e6afed5b615238",
"sha256:0e50842b2295ba8414c8c1d9d957083d5dfe9e16828b37de883f51fc53c4016f",
"sha256:0ec84b9ba0654f3b962802edc91424331f423dcf5d5f926676e0150789cb3d95",
"sha256:17067d097ed036636fa79f6a869ac26df7db1ba22039d962422506640314933a",
"sha256:1cde1753efe513705a0c6d28f5884e22bdc30438bf0085c5c486cdaff40cd67a",
"sha256:1e72728e7501a450288fc8e1f9ebc73d90cfd4671ebbd631f3e7857c39bd16f2",
"sha256:2635dbd200c2d6faf2ef9a0d04f0ecc6b13b3cad54f7c67c61155138835515d2",
"sha256:2ce46fd0b8a0c947ae047d222f7136fc4d55538741373107574271bc00e20e8f",
"sha256:34f003cb88b1ba38cb9a9a4a3161c1604973d7f9d5552c38bc2f04f829536609",
"sha256:354f373279768fa5a584bac997de6a6c9bc535c482592d7a813bb0c09be6c76f",
"sha256:38ecb5b0582cd125f67a629072fed6f83562d9dd04d7e03256c9829bdec027ad",
"sha256:3e8e01233d57639b2e30966c63d36fcea099d17c53bf424d77f088b0f4babd86",
"sha256:3f6bed7f840d44c08ebdb73b1825282b801799e325bcbdfa6bc5c370e5aecc65",
"sha256:4554eb96f0fd263041baf16cf0881b3f5dafae7a59b1049acb9540c4d57bc8cb",
"sha256:46e161722e0f619749d1cd892167039015b2c2817296104487cd03ed4a955995",
"sha256:49d9f7d256fbc804391a7f72d4a617302b1afac1112fac19b6c6cec63fe7fe8a",
"sha256:4d2f62e55a4cd9c58c1d9a1c9edaedcd857a73cb6fda875bf79093f9d9086f85",
"sha256:5f64641b42b2429f56ee08b4f427a4d2daf916ec59686061de751a55aafa22e4",
"sha256:63b92c512d9dbcc37f9d81b123dec99fdb318ba38c8059afc78086fe73820275",
"sha256:6d7696c615765091cc5093f76fd1fa069870304beaccfd58b5dcc69e55ef49c1",
"sha256:79e843d186c8fb1b102bef3e2bc35ef81160ffef3194646a7fdd6a73c6b97196",
"sha256:821eedb7165ead9eebdb569986968b541f9908979c2da8a4967ecac4439bae3d",
"sha256:84554fc53daa8f6abf8e8a66e076aff6ece62de68523d9f665f32d2fc50fd66e",
"sha256:8d83bb187fb647643bd56e1ae43f273c7f4dbcdf94550d7938cfc32566756514",
"sha256:903703372d46bce88b6920a0cd86c3ad82dae2dbef157b5fc01b70ea1cfc430f",
"sha256:9416a5c2e92ace094e9f0082c5fd473502c91651fb896bc17690d6fc475128d6",
"sha256:9a1712c015831da583b21c5bfe15e8684137097969c6d22e8316ba66b5baabe4",
"sha256:9c27f0946a3536403efb0e1c28def1ae6730a72cd0d5878db38824855e3afc44",
"sha256:a356364941fb0593bb899a1076b92dfa2029f6f5b8ba88a14fd0984aaf76d0df",
"sha256:a7039a136017eaa92c1848152827e1424701532ca8e8967fe480fe1569dae581",
"sha256:acd3a644e4807e73b4e1867b769fbf1ce8c5d80e7caaef0d90dcdc640dfc9787",
"sha256:ad0c86f3455fbd0de6c31a3056eb822fc939f81b1618f10ff3406971893b62a5",
"sha256:b4c76e3d4c56f145d41b7b6751255feefae92edbc9a61e1758a98204200f30fc",
"sha256:b6f6a8f45d0313db07d6d1d37bd0b112f887e1369758a5419c0370ba915b3871",
"sha256:c5a59996dc61835133b56a32ebe4ef3740ea5bc19b3983ac60cc32be5a665d54",
"sha256:c73aafd1afca80afecb22718f8700b40ac7cab927b8abab3c3e337d70e10e5a2",
"sha256:cee6cc0584f71adefe2c908856ccc98702baf95ff80092e4ca46061538a2ba98",
"sha256:cef04d068f5fb0518a77857953193b6bb94809a806bd0a14983a8f12ada060c9",
"sha256:cf5d1c9e6837f8af9f92b6bd3e86d513cdc11f60fd62185cc49ec7d1aba34864",
"sha256:e61155fae27570692ad1d327e81c6cf27d535a5d7ef97648a17d922224b216de",
"sha256:e7f387600d424f91576af20518334df3d97bc76a300a755f9a8d6e4f5cadd289",
"sha256:ed08d2703b5972ec736451b818c2eb9da80d66c3e84aed1deeb0c345fefe461b",
"sha256:fbd6acc766814ea6443628f4e6751d0da6593dae29c08c0b2606164db026970c",
"sha256:feff59f27338135776f6d4e2ec7aeeac5d5f7a08a83e80869121ef8164b74af9"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==1.26.4"
"version": "==2.0.0"
},
"orjson": {
"hashes": [
"sha256:0943a96b3fa09bee1afdfccc2cb236c9c64715afa375b2af296c73d91c23eab2",
"sha256:0a62f9968bab8a676a164263e485f30a0b748255ee2f4ae49a0224be95f4532b",
"sha256:16bda83b5c61586f6f788333d3cf3ed19015e3b9019188c56983b5a299210eb5",
"sha256:1770e2a0eae728b050705206d84eda8b074b65ee835e7f85c919f5705b006c9b",
"sha256:17e0713fc159abc261eea0f4feda611d32eabc35708b74bef6ad44f6c78d5ea0",
"sha256:18566beb5acd76f3769c1d1a7ec06cdb81edc4d55d2765fb677e3eaa10fa99e0",
"sha256:1952c03439e4dce23482ac846e7961f9d4ec62086eb98ae76d97bd41d72644d7",
"sha256:1bd2218d5a3aa43060efe649ec564ebedec8ce6ae0a43654b81376216d5ebd42",
"sha256:1c23dfa91481de880890d17aa7b91d586a4746a4c2aa9a145bebdbaf233768d5",
"sha256:252124b198662eee80428f1af8c63f7ff077c88723fe206a25df8dc57a57b1fa",
"sha256:2b166507acae7ba2f7c315dcf185a9111ad5e992ac81f2d507aac39193c2c818",
"sha256:2e5e176c994ce4bd434d7aafb9ecc893c15f347d3d2bbd8e7ce0b63071c52e25",
"sha256:3582b34b70543a1ed6944aca75e219e1192661a63da4d039d088a09c67543b08",
"sha256:382e52aa4270a037d41f325e7d1dfa395b7de0c367800b6f337d8157367bf3a7",
"sha256:416b195f78ae461601893f482287cee1e3059ec49b4f99479aedf22a20b1098b",
"sha256:4ad1f26bea425041e0a1adad34630c4825a9e3adec49079b1fb6ac8d36f8b754",
"sha256:4c895383b1ec42b017dd2c75ae8a5b862fc489006afde06f14afbdd0309b2af0",
"sha256:5102f50c5fc46d94f2033fe00d392588564378260d64377aec702f21a7a22912",
"sha256:520de5e2ef0b4ae546bea25129d6c7c74edb43fc6cf5213f511a927f2b28148b",
"sha256:544a12eee96e3ab828dbfcb4d5a0023aa971b27143a1d35dc214c176fdfb29b3",
"sha256:73100d9abbbe730331f2242c1fc0bcb46a3ea3b4ae3348847e5a141265479700",
"sha256:831c6ef73f9aa53c5f40ae8f949ff7681b38eaddb6904aab89dca4d85099cb78",
"sha256:8bc7a4df90da5d535e18157220d7915780d07198b54f4de0110eca6b6c11e290",
"sha256:8d0b84403d287d4bfa9bf7d1dc298d5c1c5d9f444f3737929a66f2fe4fb8f134",
"sha256:8d40c7f7938c9c2b934b297412c067936d0b54e4b8ab916fd1a9eb8f54c02294",
"sha256:9059d15c30e675a58fdcd6f95465c1522b8426e092de9fff20edebfdc15e1cb0",
"sha256:93433b3c1f852660eb5abdc1f4dd0ced2be031ba30900433223b28ee0140cde5",
"sha256:978be58a68ade24f1af7758626806e13cff7748a677faf95fbb298359aa1e20d",
"sha256:99b880d7e34542db89f48d14ddecbd26f06838b12427d5a25d71baceb5ba119d",
"sha256:9a7bc9e8bc11bac40f905640acd41cbeaa87209e7e1f57ade386da658092dc16",
"sha256:9e253498bee561fe85d6325ba55ff2ff08fb5e7184cd6a4d7754133bd19c9195",
"sha256:9f3e87733823089a338ef9bbf363ef4de45e5c599a9bf50a7a9b82e86d0228da",
"sha256:9fb6c3f9f5490a3eb4ddd46fc1b6eadb0d6fc16fb3f07320149c3286a1409dd8",
"sha256:a39aa73e53bec8d410875683bfa3a8edf61e5a1c7bb4014f65f81d36467ea098",
"sha256:b69a58a37dab856491bf2d3bbf259775fdce262b727f96aafbda359cb1d114d8",
"sha256:b8d4d1a6868cde356f1402c8faeb50d62cee765a1f7ffcfd6de732ab0581e063",
"sha256:ba7f67aa7f983c4345eeda16054a4677289011a478ca947cd69c0a86ea45e534",
"sha256:be2719e5041e9fb76c8c2c06b9600fe8e8584e6980061ff88dcbc2691a16d20d",
"sha256:be2aab54313752c04f2cbaab4515291ef5af8c2256ce22abc007f89f42f49109",
"sha256:c0403ed9c706dcd2809f1600ed18f4aae50be263bd7112e54b50e2c2bc3ebd6d",
"sha256:c8334c0d87103bb9fbbe59b78129f1f40d1d1e8355bbed2ca71853af15fa4ed3",
"sha256:cb0175a5798bdc878956099f5c54b9837cb62cfbf5d0b86ba6d77e43861bcec2",
"sha256:ccaa0a401fc02e8828a5bedfd80f8cd389d24f65e5ca3954d72c6582495b4bcf",
"sha256:cf20465e74c6e17a104ecf01bf8cd3b7b252565b4ccee4548f18b012ff2f8069",
"sha256:d4a654ec1de8fdaae1d80d55cee65893cb06494e124681ab335218be6a0691e7",
"sha256:e852baafceff8da3c9defae29414cc8513a1586ad93e45f27b89a639c68e8176"
"sha256:03b565c3b93f5d6e001db48b747d31ea3819b89abf041ee10ac6988886d18e01",
"sha256:099e81a5975237fda3100f918839af95f42f981447ba8f47adb7b6a3cdb078fa",
"sha256:10c0eb7e0c75e1e486c7563fe231b40fdd658a035ae125c6ba651ca3b07936f5",
"sha256:1146bf85ea37ac421594107195db8bc77104f74bc83e8ee21a2e58596bfb2f04",
"sha256:1670fe88b116c2745a3a30b0f099b699a02bb3482c2591514baf5433819e4f4d",
"sha256:185c394ef45b18b9a7d8e8f333606e2e8194a50c6e3c664215aae8cf42c5385e",
"sha256:1ad1de7fef79736dde8c3554e75361ec351158a906d747bd901a52a5c9c8d24b",
"sha256:235dadefb793ad12f7fa11e98a480db1f7c6469ff9e3da5e73c7809c700d746b",
"sha256:28afa96f496474ce60d3340fe8d9a263aa93ea01201cd2bad844c45cd21f5268",
"sha256:2d97531cdfe9bdd76d492e69800afd97e5930cb0da6a825646667b2c6c6c0211",
"sha256:338fd4f071b242f26e9ca802f443edc588fa4ab60bfa81f38beaedf42eda226c",
"sha256:36a10f43c5f3a55c2f680efe07aa93ef4a342d2960dd2b1b7ea2dd764fe4a37c",
"sha256:3d21b9983da032505f7050795e98b5d9eee0df903258951566ecc358f6696969",
"sha256:51bbcdea96cdefa4a9b4461e690c75ad4e33796530d182bdd5c38980202c134a",
"sha256:53ed1c879b10de56f35daf06dbc4a0d9a5db98f6ee853c2dbd3ee9d13e6f302f",
"sha256:545d493c1f560d5ccfc134803ceb8955a14c3fcb47bbb4b2fee0232646d0b932",
"sha256:584c902ec19ab7928fd5add1783c909094cc53f31ac7acfada817b0847975f26",
"sha256:5a35455cc0b0b3a1eaf67224035f5388591ec72b9b6136d66b49a553ce9eb1e6",
"sha256:5df58d206e78c40da118a8c14fc189207fffdcb1f21b3b4c9c0c18e839b5a214",
"sha256:64c9cc089f127e5875901ac05e5c25aa13cfa5dbbbd9602bda51e5c611d6e3e2",
"sha256:68f85ecae7af14a585a563ac741b0547a3f291de81cd1e20903e79f25170458f",
"sha256:6970ed7a3126cfed873c5d21ece1cd5d6f83ca6c9afb71bbae21a0b034588d96",
"sha256:6b68742c469745d0e6ca5724506858f75e2f1e5b59a4315861f9e2b1df77775a",
"sha256:7a5baef8a4284405d96c90c7c62b755e9ef1ada84c2406c24a9ebec86b89f46d",
"sha256:7d10cc1b594951522e35a3463da19e899abe6ca95f3c84c69e9e901e0bd93d38",
"sha256:85c89131d7b3218db1b24c4abecea92fd6c7f9fab87441cfc342d3acc725d807",
"sha256:8a11d459338f96a9aa7f232ba95679fc0c7cedbd1b990d736467894210205c09",
"sha256:8c13ca5e2ddded0ce6a927ea5a9f27cae77eee4c75547b4297252cb20c4d30e6",
"sha256:9cd684927af3e11b6e754df80b9ffafd9fb6adcaa9d3e8fdd5891be5a5cad51e",
"sha256:b2efbd67feff8c1f7728937c0d7f6ca8c25ec81373dc8db4ef394c1d93d13dc5",
"sha256:b39e006b00c57125ab974362e740c14a0c6a66ff695bff44615dcf4a70ce2b86",
"sha256:b6c8e30adfa52c025f042a87f450a6b9ea29649d828e0fec4858ed5e6caecf63",
"sha256:be79e2393679eda6a590638abda16d167754393f5d0850dcbca2d0c3735cebe2",
"sha256:c05f16701ab2a4ca146d0bca950af254cb7c02f3c01fca8efbbad82d23b3d9d4",
"sha256:c4057c3b511bb8aef605616bd3f1f002a697c7e4da6adf095ca5b84c0fd43595",
"sha256:c4a65310ccb5c9910c47b078ba78e2787cb3878cdded1702ac3d0da71ddc5228",
"sha256:ca0b3a94ac8d3886c9581b9f9de3ce858263865fdaa383fbc31c310b9eac07c9",
"sha256:cc28e90a7cae7fcba2493953cff61da5a52950e78dc2dacfe931a317ee3d8de7",
"sha256:cdf7365063e80899ae3a697def1277c17a7df7ccfc979990a403dfe77bb54d40",
"sha256:d69858c32f09c3e1ce44b617b3ebba1aba030e777000ebdf72b0d8e365d0b2b3",
"sha256:dbead71dbe65f959b7bd8cf91e0e11d5338033eba34c114f69078d59827ee139",
"sha256:dcbe82b35d1ac43b0d84072408330fd3295c2896973112d495e7234f7e3da2e1",
"sha256:dfc91d4720d48e2a709e9c368d5125b4b5899dced34b5400c3837dadc7d6271b",
"sha256:eded5138cc565a9d618e111c6d5c2547bbdd951114eb822f7f6309e04db0fb47",
"sha256:f4324929c2dd917598212bfd554757feca3e5e0fa60da08be11b4aa8b90013c1",
"sha256:fb66215277a230c456f9038d5e2d84778141643207f85336ef8d2a9da26bd7ca"
],
"markers": "python_version >= '3.8'",
"version": "==3.10.3"
"version": "==3.10.5"
},
"osmpythontools": {
"hashes": [
@ -769,11 +778,11 @@
},
"packaging": {
"hashes": [
"sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5",
"sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"
"sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002",
"sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"
],
"markers": "python_version >= '3.7'",
"version": "==24.0"
"markers": "python_version >= '3.8'",
"version": "==24.1"
},
"pandas": {
"hashes": [
@ -887,96 +896,97 @@
},
"pydantic": {
"hashes": [
"sha256:71b2945998f9c9b7919a45bde9a50397b289937d215ae141c1d0903ba7149fd7",
"sha256:834ab954175f94e6e68258537dc49402c4a5e9d0409b9f1b86b7e934a8372de7"
"sha256:0c84efd9548d545f63ac0060c1e4d39bb9b14db8b3c0652338aecc07b5adec52",
"sha256:ee8538d41ccb9c0a9ad3e0e5f07bf15ed8015b481ced539a1759d8cc89ae90d0"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==2.7.2"
"version": "==2.7.4"
},
"pydantic-core": {
"hashes": [
"sha256:0bee9bb305a562f8b9271855afb6ce00223f545de3d68560b3c1649c7c5295e9",
"sha256:0ecce4b2360aa3f008da3327d652e74a0e743908eac306198b47e1c58b03dd2b",
"sha256:17954d784bf8abfc0ec2a633108207ebc4fa2df1a0e4c0c3ccbaa9bb01d2c426",
"sha256:19d2e725de0f90d8671f89e420d36c3dd97639b98145e42fcc0e1f6d492a46dc",
"sha256:1f9cd7f5635b719939019be9bda47ecb56e165e51dd26c9a217a433e3d0d59a9",
"sha256:200ad4e3133cb99ed82342a101a5abf3d924722e71cd581cc113fe828f727fbc",
"sha256:24b214b7ee3bd3b865e963dbed0f8bc5375f49449d70e8d407b567af3222aae4",
"sha256:2c44efdd3b6125419c28821590d7ec891c9cb0dff33a7a78d9d5c8b6f66b9702",
"sha256:2c8333f6e934733483c7eddffdb094c143b9463d2af7e6bd85ebcb2d4a1b82c6",
"sha256:2f7ef5f0ebb77ba24c9970da18b771711edc5feaf00c10b18461e0f5f5949231",
"sha256:304378b7bf92206036c8ddd83a2ba7b7d1a5b425acafff637172a3aa72ad7083",
"sha256:370059b7883485c9edb9655355ff46d912f4b03b009d929220d9294c7fd9fd60",
"sha256:37b40c05ced1ba4218b14986fe6f283d22e1ae2ff4c8e28881a70fb81fbfcda7",
"sha256:3d3e42bb54e7e9d72c13ce112e02eb1b3b55681ee948d748842171201a03a98a",
"sha256:3fc1c7f67f34c6c2ef9c213e0f2a351797cda98249d9ca56a70ce4ebcaba45f4",
"sha256:41dbdcb0c7252b58fa931fec47937edb422c9cb22528f41cb8963665c372caf6",
"sha256:432e999088d85c8f36b9a3f769a8e2b57aabd817bbb729a90d1fe7f18f6f1f39",
"sha256:45e4ffbae34f7ae30d0047697e724e534a7ec0a82ef9994b7913a412c21462a0",
"sha256:4afa5f5973e8572b5c0dcb4e2d4fda7890e7cd63329bd5cc3263a25c92ef0026",
"sha256:544a9a75622357076efb6b311983ff190fbfb3c12fc3a853122b34d3d358126c",
"sha256:5560dda746c44b48bf82b3d191d74fe8efc5686a9ef18e69bdabccbbb9ad9442",
"sha256:58ff8631dbab6c7c982e6425da8347108449321f61fe427c52ddfadd66642af7",
"sha256:5a64faeedfd8254f05f5cf6fc755023a7e1606af3959cfc1a9285744cc711044",
"sha256:60e4c625e6f7155d7d0dcac151edf5858102bc61bf959d04469ca6ee4e8381bd",
"sha256:616221a6d473c5b9aa83fa8982745441f6a4a62a66436be9445c65f241b86c94",
"sha256:63081a49dddc6124754b32a3774331467bfc3d2bd5ff8f10df36a95602560361",
"sha256:666e45cf071669fde468886654742fa10b0e74cd0fa0430a46ba6056b24fb0af",
"sha256:67bc078025d70ec5aefe6200ef094576c9d86bd36982df1301c758a9fff7d7f4",
"sha256:691018785779766127f531674fa82bb368df5b36b461622b12e176c18e119022",
"sha256:6a36f78674cbddc165abab0df961b5f96b14461d05feec5e1f78da58808b97e7",
"sha256:6afd5c867a74c4d314c557b5ea9520183fadfbd1df4c2d6e09fd0d990ce412cd",
"sha256:6b32c2a1f8032570842257e4c19288eba9a2bba4712af542327de9a1204faff8",
"sha256:6e59fca51ffbdd1638b3856779342ed69bcecb8484c1d4b8bdb237d0eb5a45e2",
"sha256:70cf099197d6b98953468461d753563b28e73cf1eade2ffe069675d2657ed1d5",
"sha256:73038d66614d2e5cde30435b5afdced2b473b4c77d4ca3a8624dd3e41a9c19be",
"sha256:744697428fcdec6be5670460b578161d1ffe34743a5c15656be7ea82b008197c",
"sha256:77319771a026f7c7d29c6ebc623de889e9563b7087911b46fd06c044a12aa5e9",
"sha256:7a20dded653e516a4655f4c98e97ccafb13753987434fe7cf044aa25f5b7d417",
"sha256:7e6382ce89a92bc1d0c0c5edd51e931432202b9080dc921d8d003e616402efd1",
"sha256:7fdd362f6a586e681ff86550b2379e532fee63c52def1c666887956748eaa326",
"sha256:80aea0ffeb1049336043d07799eace1c9602519fb3192916ff525b0287b2b1e4",
"sha256:82f2718430098bcdf60402136c845e4126a189959d103900ebabb6774a5d9fdb",
"sha256:855ec66589c68aa367d989da5c4755bb74ee92ccad4fdb6af942c3612c067e34",
"sha256:9128089da8f4fe73f7a91973895ebf2502539d627891a14034e45fb9e707e26d",
"sha256:929c24e9dea3990bc8bcd27c5f2d3916c0c86f5511d2caa69e0d5290115344a9",
"sha256:98ed737567d8f2ecd54f7c8d4f8572ca7c7921ede93a2e52939416170d357812",
"sha256:9a46795b1f3beb167eaee91736d5d17ac3a994bf2215a996aed825a45f897558",
"sha256:9f9e04afebd3ed8c15d67a564ed0a34b54e52136c6d40d14c5547b238390e779",
"sha256:a4e651e47d981c1b701dcc74ab8fec5a60a5b004650416b4abbef13db23bc7be",
"sha256:a62e437d687cc148381bdd5f51e3e81f5b20a735c55f690c5be94e05da2b0d5c",
"sha256:aaee40f25bba38132e655ffa3d1998a6d576ba7cf81deff8bfa189fb43fd2bbe",
"sha256:adf952c3f4100e203cbaf8e0c907c835d3e28f9041474e52b651761dc248a3c0",
"sha256:b367a73a414bbb08507da102dc2cde0fa7afe57d09b3240ce82a16d608a7679c",
"sha256:b8e20e15d18bf7dbb453be78a2d858f946f5cdf06c5072453dace00ab652e2b2",
"sha256:b95a0972fac2b1ff3c94629fc9081b16371dad870959f1408cc33b2f78ad347a",
"sha256:b9ebe8231726c49518b16b237b9fe0d7d361dd221302af511a83d4ada01183ab",
"sha256:ba905d184f62e7ddbb7a5a751d8a5c805463511c7b08d1aca4a3e8c11f2e5048",
"sha256:bd4435b8d83f0c9561a2a9585b1de78f1abb17cb0cef5f39bf6a4b47d19bafe3",
"sha256:bd7df92f28d351bb9f12470f4c533cf03d1b52ec5a6e5c58c65b183055a60106",
"sha256:c0037a92cf0c580ed14e10953cdd26528e8796307bb8bb312dc65f71547df04d",
"sha256:c0d9ff283cd3459fa0bf9b0256a2b6f01ac1ff9ffb034e24457b9035f75587cb",
"sha256:c56eca1686539fa0c9bda992e7bd6a37583f20083c37590413381acfc5f192d6",
"sha256:c6ac9ffccc9d2e69d9fba841441d4259cb668ac180e51b30d3632cd7abca2b9b",
"sha256:c826870b277143e701c9ccf34ebc33ddb4d072612683a044e7cce2d52f6c3fef",
"sha256:cd4a032bb65cc132cae1fe3e52877daecc2097965cd3914e44fbd12b00dae7c5",
"sha256:d33ce258e4e6e6038f2b9e8b8a631d17d017567db43483314993b3ca345dcbbb",
"sha256:d531076bdfb65af593326ffd567e6ab3da145020dafb9187a1d131064a55f97c",
"sha256:dccf3ef1400390ddd1fb55bf0632209d39140552d068ee5ac45553b556780e06",
"sha256:df11fa992e9f576473038510d66dd305bcd51d7dd508c163a8c8fe148454e059",
"sha256:e1a8376fef60790152564b0eab376b3e23dd6e54f29d84aad46f7b264ecca943",
"sha256:e201935d282707394f3668380e41ccf25b5794d1b131cdd96b07f615a33ca4b1",
"sha256:e2e253af04ceaebde8eb201eb3f3e3e7e390f2d275a88300d6a1959d710539e2",
"sha256:e862823be114387257dacbfa7d78547165a85d7add33b446ca4f4fae92c7ff5c",
"sha256:eecf63195be644b0396f972c82598cd15693550f0ff236dcf7ab92e2eb6d3522",
"sha256:f0928cde2ae416a2d1ebe6dee324709c6f73e93494d8c7aea92df99aab1fc40f",
"sha256:f9c08cabff68704a1b4667d33f534d544b8a07b8e5d039c37067fceb18789e78",
"sha256:fec02527e1e03257aa25b1a4dcbe697b40a22f1229f5d026503e8b7ff6d2eda7",
"sha256:ff58f379345603d940e461eae474b6bbb6dab66ed9a851ecd3cb3709bf4dcf6a",
"sha256:ffecbb5edb7f5ffae13599aec33b735e9e4c7676ca1633c60f2c606beb17efc5"
"sha256:01dd777215e2aa86dfd664daed5957704b769e726626393438f9c87690ce78c3",
"sha256:0eb2a4f660fcd8e2b1c90ad566db2b98d7f3f4717c64fe0a83e0adb39766d5b8",
"sha256:0fbbdc827fe5e42e4d196c746b890b3d72876bdbf160b0eafe9f0334525119c8",
"sha256:123c3cec203e3f5ac7b000bd82235f1a3eced8665b63d18be751f115588fea30",
"sha256:14601cdb733d741b8958224030e2bfe21a4a881fb3dd6fbb21f071cabd48fa0a",
"sha256:18f469a3d2a2fdafe99296a87e8a4c37748b5080a26b806a707f25a902c040a8",
"sha256:19894b95aacfa98e7cb093cd7881a0c76f55731efad31073db4521e2b6ff5b7d",
"sha256:1b4de2e51bbcb61fdebd0ab86ef28062704f62c82bbf4addc4e37fa4b00b7cbc",
"sha256:1d886dc848e60cb7666f771e406acae54ab279b9f1e4143babc9c2258213daa2",
"sha256:1f4d26ceb5eb9eed4af91bebeae4b06c3fb28966ca3a8fb765208cf6b51102ab",
"sha256:21a5e440dbe315ab9825fcd459b8814bb92b27c974cbc23c3e8baa2b76890077",
"sha256:293afe532740370aba8c060882f7d26cfd00c94cae32fd2e212a3a6e3b7bc15e",
"sha256:2f5966897e5461f818e136b8451d0551a2e77259eb0f73a837027b47dc95dab9",
"sha256:2fd41f6eff4c20778d717af1cc50eca52f5afe7805ee530a4fbd0bae284f16e9",
"sha256:2fdf2156aa3d017fddf8aea5adfba9f777db1d6022d392b682d2a8329e087cef",
"sha256:3c40d4eaad41f78e3bbda31b89edc46a3f3dc6e171bf0ecf097ff7a0ffff7cb1",
"sha256:43d447dd2ae072a0065389092a231283f62d960030ecd27565672bd40746c507",
"sha256:44a688331d4a4e2129140a8118479443bd6f1905231138971372fcde37e43528",
"sha256:44c7486a4228413c317952e9d89598bcdfb06399735e49e0f8df643e1ccd0558",
"sha256:44cd83ab6a51da80fb5adbd9560e26018e2ac7826f9626bc06ca3dc074cd198b",
"sha256:46387e38bd641b3ee5ce247563b60c5ca098da9c56c75c157a05eaa0933ed154",
"sha256:4701b19f7e3a06ea655513f7938de6f108123bf7c86bbebb1196eb9bd35cf724",
"sha256:4748321b5078216070b151d5271ef3e7cc905ab170bbfd27d5c83ee3ec436695",
"sha256:4b06beb3b3f1479d32befd1f3079cc47b34fa2da62457cdf6c963393340b56e9",
"sha256:4d0dcc59664fcb8974b356fe0a18a672d6d7cf9f54746c05f43275fc48636851",
"sha256:4e99bc050fe65c450344421017f98298a97cefc18c53bb2f7b3531eb39bc7805",
"sha256:509daade3b8649f80d4e5ff21aa5673e4ebe58590b25fe42fac5f0f52c6f034a",
"sha256:51991a89639a912c17bef4b45c87bd83593aee0437d8102556af4885811d59f5",
"sha256:53db086f9f6ab2b4061958d9c276d1dbe3690e8dd727d6abf2321d6cce37fa94",
"sha256:564d7922e4b13a16b98772441879fcdcbe82ff50daa622d681dd682175ea918c",
"sha256:574d92eac874f7f4db0ca653514d823a0d22e2354359d0759e3f6a406db5d55d",
"sha256:578e24f761f3b425834f297b9935e1ce2e30f51400964ce4801002435a1b41ef",
"sha256:59ff3e89f4eaf14050c8022011862df275b552caef8082e37b542b066ce1ff26",
"sha256:5f09baa656c904807e832cf9cce799c6460c450c4ad80803517032da0cd062e2",
"sha256:6891a2ae0e8692679c07728819b6e2b822fb30ca7445f67bbf6509b25a96332c",
"sha256:6a750aec7bf431517a9fd78cb93c97b9b0c496090fee84a47a0d23668976b4b0",
"sha256:6f5c4d41b2771c730ea1c34e458e781b18cc668d194958e0112455fff4e402b2",
"sha256:77450e6d20016ec41f43ca4a6c63e9fdde03f0ae3fe90e7c27bdbeaece8b1ed4",
"sha256:81b5efb2f126454586d0f40c4d834010979cb80785173d1586df845a632e4e6d",
"sha256:823be1deb01793da05ecb0484d6c9e20baebb39bd42b5d72636ae9cf8350dbd2",
"sha256:834b5230b5dfc0c1ec37b2fda433b271cbbc0e507560b5d1588e2cc1148cf1ce",
"sha256:847a35c4d58721c5dc3dba599878ebbdfd96784f3fb8bb2c356e123bdcd73f34",
"sha256:86110d7e1907ab36691f80b33eb2da87d780f4739ae773e5fc83fb272f88825f",
"sha256:8951eee36c57cd128f779e641e21eb40bc5073eb28b2d23f33eb0ef14ffb3f5d",
"sha256:8a7164fe2005d03c64fd3b85649891cd4953a8de53107940bf272500ba8a788b",
"sha256:8b8bab4c97248095ae0c4455b5a1cd1cdd96e4e4769306ab19dda135ea4cdb07",
"sha256:90afc12421df2b1b4dcc975f814e21bc1754640d502a2fbcc6d41e77af5ec312",
"sha256:938cb21650855054dc54dfd9120a851c974f95450f00683399006aa6e8abb057",
"sha256:942ba11e7dfb66dc70f9ae66b33452f51ac7bb90676da39a7345e99ffb55402d",
"sha256:972658f4a72d02b8abfa2581d92d59f59897d2e9f7e708fdabe922f9087773af",
"sha256:97736815b9cc893b2b7f663628e63f436018b75f44854c8027040e05230eeddb",
"sha256:98906207f29bc2c459ff64fa007afd10a8c8ac080f7e4d5beff4c97086a3dabd",
"sha256:99457f184ad90235cfe8461c4d70ab7dd2680e28821c29eca00252ba90308c78",
"sha256:a0d829524aaefdebccb869eed855e2d04c21d2d7479b6cada7ace5448416597b",
"sha256:a2fdd81edd64342c85ac7cf2753ccae0b79bf2dfa063785503cb85a7d3593223",
"sha256:a55b5b16c839df1070bc113c1f7f94a0af4433fcfa1b41799ce7606e5c79ce0a",
"sha256:a642295cd0c8df1b86fc3dced1d067874c353a188dc8e0f744626d49e9aa51c4",
"sha256:ab86ce7c8f9bea87b9d12c7f0af71102acbf5ecbc66c17796cff45dae54ef9a5",
"sha256:abc267fa9837245cc28ea6929f19fa335f3dc330a35d2e45509b6566dc18be23",
"sha256:ae1d6df168efb88d7d522664693607b80b4080be6750c913eefb77e34c12c71a",
"sha256:b2ebef0e0b4454320274f5e83a41844c63438fdc874ea40a8b5b4ecb7693f1c4",
"sha256:b48ece5bde2e768197a2d0f6e925f9d7e3e826f0ad2271120f8144a9db18d5c8",
"sha256:b7cdf28938ac6b8b49ae5e92f2735056a7ba99c9b110a474473fd71185c1af5d",
"sha256:bb4462bd43c2460774914b8525f79b00f8f407c945d50881568f294c1d9b4443",
"sha256:bc4ff9805858bd54d1a20efff925ccd89c9d2e7cf4986144b30802bf78091c3e",
"sha256:c1322d7dd74713dcc157a2b7898a564ab091ca6c58302d5c7b4c07296e3fd00f",
"sha256:c67598100338d5d985db1b3d21f3619ef392e185e71b8d52bceacc4a7771ea7e",
"sha256:ca26a1e73c48cfc54c4a76ff78df3727b9d9f4ccc8dbee4ae3f73306a591676d",
"sha256:d323a01da91851a4f17bf592faf46149c9169d68430b3146dcba2bb5e5719abc",
"sha256:dc1803ac5c32ec324c5261c7209e8f8ce88e83254c4e1aebdc8b0a39f9ddb443",
"sha256:e00a3f196329e08e43d99b79b286d60ce46bed10f2280d25a1718399457e06be",
"sha256:e85637bc8fe81ddb73fda9e56bab24560bdddfa98aa64f87aaa4e4b6730c23d2",
"sha256:e858ac0a25074ba4bce653f9b5d0a85b7456eaddadc0ce82d3878c22489fa4ee",
"sha256:eae237477a873ab46e8dd748e515c72c0c804fb380fbe6c85533c7de51f23a8f",
"sha256:ebef0dd9bf9b812bf75bda96743f2a6c5734a02092ae7f721c048d156d5fabae",
"sha256:ec3beeada09ff865c344ff3bc2f427f5e6c26401cc6113d77e372c3fdac73864",
"sha256:f76d0ad001edd426b92233d45c746fd08f467d56100fd8f30e9ace4b005266e4",
"sha256:f85d05aa0918283cf29a30b547b4df2fbb56b45b135f9e35b6807cb28bc47951",
"sha256:f9899c94762343f2cc2fc64c13e7cae4c3cc65cdfc87dd810a31654c9b7358cc"
],
"markers": "python_version >= '3.8'",
"version": "==2.18.3"
"version": "==2.18.4"
},
"pygments": {
"hashes": [
@ -1120,6 +1130,54 @@
"markers": "python_version >= '3.9'",
"version": "==1.13.1"
},
"shapely": {
"hashes": [
"sha256:011b77153906030b795791f2fdfa2d68f1a8d7e40bce78b029782ade3afe4f2f",
"sha256:03152442d311a5e85ac73b39680dd64a9892fa42bb08fd83b3bab4fe6999bfa0",
"sha256:05ffd6491e9e8958b742b0e2e7c346635033d0a5f1a0ea083547fcc854e5d5cf",
"sha256:0776c92d584f72f1e584d2e43cfc5542c2f3dd19d53f70df0900fda643f4bae6",
"sha256:263bcf0c24d7a57c80991e64ab57cba7a3906e31d2e21b455f493d4aab534aaa",
"sha256:2fbdc1140a7d08faa748256438291394967aa54b40009f54e8d9825e75ef6113",
"sha256:30982f79f21bb0ff7d7d4a4e531e3fcaa39b778584c2ce81a147f95be1cd58c9",
"sha256:31c19a668b5a1eadab82ff070b5a260478ac6ddad3a5b62295095174a8d26398",
"sha256:3f9103abd1678cb1b5f7e8e1af565a652e036844166c91ec031eeb25c5ca8af0",
"sha256:41388321a73ba1a84edd90d86ecc8bfed55e6a1e51882eafb019f45895ec0f65",
"sha256:4310b5494271e18580d61022c0857eb85d30510d88606fa3b8314790df7f367d",
"sha256:464157509ce4efa5ff285c646a38b49f8c5ef8d4b340f722685b09bb033c5ccf",
"sha256:485246fcdb93336105c29a5cfbff8a226949db37b7473c89caa26c9bae52a242",
"sha256:489c19152ec1f0e5c5e525356bcbf7e532f311bff630c9b6bc2db6f04da6a8b9",
"sha256:4f2ab0faf8188b9f99e6a273b24b97662194160cc8ca17cf9d1fb6f18d7fb93f",
"sha256:55a38dcd1cee2f298d8c2ebc60fc7d39f3b4535684a1e9e2f39a80ae88b0cea7",
"sha256:58b0ecc505bbe49a99551eea3f2e8a9b3b24b3edd2a4de1ac0dc17bc75c9ec07",
"sha256:5af4cd0d8cf2912bd95f33586600cac9c4b7c5053a036422b97cfe4728d2eb53",
"sha256:5bbd974193e2cc274312da16b189b38f5f128410f3377721cadb76b1e8ca5328",
"sha256:5c4849916f71dc44e19ed370421518c0d86cf73b26e8656192fcfcda08218fbd",
"sha256:5dc736127fac70009b8d309a0eeb74f3e08979e530cf7017f2f507ef62e6cfb8",
"sha256:63f3a80daf4f867bd80f5c97fbe03314348ac1b3b70fb1c0ad255a69e3749879",
"sha256:674d7baf0015a6037d5758496d550fc1946f34bfc89c1bf247cabdc415d7747e",
"sha256:6cd4ccecc5ea5abd06deeaab52fcdba372f649728050c6143cc405ee0c166679",
"sha256:790a168a808bd00ee42786b8ba883307c0e3684ebb292e0e20009588c426da47",
"sha256:7d56ce3e2a6a556b59a288771cf9d091470116867e578bebced8bfc4147fbfd7",
"sha256:841f93a0e31e4c64d62ea570d81c35de0f6cea224568b2430d832967536308e6",
"sha256:8de4578e838a9409b5b134a18ee820730e507b2d21700c14b71a2b0757396acc",
"sha256:92a41d936f7d6743f343be265ace93b7c57f5b231e21b9605716f5a47c2879e7",
"sha256:9831816a5d34d5170aa9ed32a64982c3d6f4332e7ecfe62dc97767e163cb0b17",
"sha256:994c244e004bc3cfbea96257b883c90a86e8cbd76e069718eb4c6b222a56f78b",
"sha256:9dab4c98acfb5fb85f5a20548b5c0abe9b163ad3525ee28822ffecb5c40e724c",
"sha256:b79bbd648664aa6f44ef018474ff958b6b296fed5c2d42db60078de3cffbc8aa",
"sha256:c3e700abf4a37b7b8b90532fa6ed5c38a9bfc777098bc9fbae5ec8e618ac8f30",
"sha256:c52ed79f683f721b69a10fb9e3d940a468203f5054927215586c5d49a072de8d",
"sha256:c75c98380b1ede1cae9a252c6dc247e6279403fae38c77060a5e6186c95073ac",
"sha256:d2b4431f522b277c79c34b65da128029a9955e4481462cbf7ebec23aab61fc58",
"sha256:ddf4a9bfaac643e62702ed662afc36f6abed2a88a21270e891038f9a19bc08fc",
"sha256:de0205cb21ad5ddaef607cda9a3191eadd1e7a62a756ea3a356369675230ac35",
"sha256:ec555c9d0db12d7fd777ba3f8b75044c73e576c720a851667432fabb7057da6c",
"sha256:fb5cdcbbe3080181498931b52a91a21a781a35dcb859da741c0345c6402bf00c"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==2.0.4"
},
"shellingham": {
"hashes": [
"sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686",
@ -1170,11 +1228,11 @@
},
"typing-extensions": {
"hashes": [
"sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8",
"sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"
"sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d",
"sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"
],
"markers": "python_version >= '3.8'",
"version": "==4.12.0"
"version": "==4.12.2"
},
"tzdata": {
"hashes": [
@ -1273,11 +1331,11 @@
"standard"
],
"hashes": [
"sha256:78fa0b5f56abb8562024a59041caeb555c86e48d0efdd23c3fe7de7a4075bdab",
"sha256:f678dec4fa3a39706bbf49b9ec5fc40049d42418716cea52b53f07828a60aa37"
"sha256:cd17daa7f3b9d7a24de3617820e634d0933b69eed8e33a516071174427238c81",
"sha256:d46cd8e0fd80240baffbcd9ec1012a712938754afcf81bce56c024c1656aece8"
],
"markers": "python_version >= '3.8'",
"version": "==0.30.0"
"version": "==0.30.1"
},
"uvloop": {
"hashes": [
@ -1474,11 +1532,11 @@
},
"xarray": {
"hashes": [
"sha256:7ddedfe2294a0ab00f02d0fbdcb9c6300ec589f3cf436a9c7b7b577a12cd9bcf",
"sha256:e0eb1cb265f265126795f388ed9591f3c752f2aca491f6c0576711fd15b708f2"
"sha256:0b91e0bc4dc0296947947640fe31ec6e867ce258d2f7cbc10bedf4a6d68340c7",
"sha256:721a7394e8ec3d592b2d8ebe21eed074ac077dc1bb1bd777ce00e41700b4866c"
],
"markers": "python_version >= '3.9'",
"version": "==2024.5.0"
"version": "==2024.6.0"
}
},
"develop": {}

View File

@ -1 +0,0 @@
import app.src

View File

@ -1,34 +0,0 @@
from .src.optimizer import solve_optimization
from .src.landmarks_manager import Landmark
from fastapi import FastAPI
app = FastAPI()
@app.get("/optimize/{max_steps}/{print_details}")
def main(max_steps: int, print_details: bool):
# CONSTRAINT TO RESPECT MAX NUMBER OF STEPS
#max_steps = 16
# Initialize all landmarks (+ start and goal). Order matters here
landmarks = []
landmarks.append(Landmark("départ", -1, (0, 0)))
landmarks.append(Landmark("tour eiffel", 99, (0,2))) # PUT IN JSON
landmarks.append(Landmark("arc de triomphe", 99, (0,4)))
landmarks.append(Landmark("louvre", 99, (0,6)))
landmarks.append(Landmark("montmartre", 99, (0,10)))
landmarks.append(Landmark("concorde", 99, (0,8)))
landmarks.append(Landmark("arrivée", -1, (0, 0)))
visiting_order = solve_optimization(landmarks, max_steps, print_details)
#return visiting_order
return("max steps :", max_steps, "\n", visiting_order)
"""if __name__ == "__main__":
main()"""

View File

@ -1,57 +0,0 @@
from OSMPythonTools.api import Api
from OSMPythonTools.overpass import Overpass
from dataclasses import dataclass
# Defines the landmark class (aka some place there is to visit)
@dataclass
class Landmarkkkk :
name : str
attractiveness : int
id : int
@dataclass
class Landmark :
name : str
attractiveness : int
loc : tuple
# Converts a OSM id to a landmark
def add_from_id(id: int, score: int) :
try :
s = 'way/' + str(id) # prepare string for query
obj = api.query(s) # object to add
except :
s = 'relation/' + str(id) # prepare string for query
obj = api.query(s) # object to add
return Landmarkkkk(obj.tag('name:fr'), score, id) # create Landmark out of it
# take a lsit of tuples (id, score) to generate a list of landmarks
def generate_landmarks(ids_and_scores: list) :
L = []
for tup in ids_and_scores :
L.append(add_from_id(tup[0], tup[1]))
return L
api = Api()
l = (7515426, 70)
t = (5013364, 100)
n = (201611261, 99)
a = (226413508, 50)
m = (23762981, 30)
ids_and_scores = [t, l, n, a, m]
landmarks = generate_landmarks(ids_and_scores)
for obj in landmarks :
print(obj)

View File

@ -1,323 +0,0 @@
from scipy.optimize import linprog
import numpy as np
from scipy.linalg import block_diag
# landmarks = [Landmark_1, Landmark_2, ...]
# Convert the solution of the optimization into the list of edges to follow. Order is taken into account
def untangle(resx: list) :
N = len(resx) # length of res
L = int(np.sqrt(N)) # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
n_edges = resx.sum() # number of edges
order = []
nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0]
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
indx = nonzero_tup[0].tolist()
indy = nonzero_tup[1].tolist()
vert = (indx[0], indy[0])
order.append(vert[0])
order.append(vert[1])
while len(order) < n_edges + 1 :
ind = indx.index(vert[1])
vert = (indx[ind], indy[ind])
order.append(vert[1])
return order
# Just to print the result
def print_res(res, landmarks: list, P) :
X = abs(res.x)
order = untangle(X)
things = []
"""N = int(np.sqrt(len(X)))
for i in range(N):
print(X[i*N:i*N+N])
print("Optimal value:", -res.fun) # Minimization, so we negate to get the maximum
print("Optimal point:", res.x)
for i,x in enumerate(X) : X[i] = round(x,0)
print(order)"""
if (X.sum()+1)**2 == len(X) :
print('\nAll landmarks can be visited within max_steps, the following order is suggested : ')
else :
print('Could not visit all the landmarks, the following order is suggested : ')
for idx in order :
print('- ' + landmarks[idx].name)
things.append(landmarks[idx].name)
steps = path_length(P, abs(res.x))
print("\nSteps walked : " + str(steps))
return things
# Checks for cases of circular symmetry in the result
def has_circle(resx: list) :
N = len(resx) # length of res
L = int(np.sqrt(N)) # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
n_edges = resx.sum() # number of edges
nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0]
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
indx = nonzero_tup[0].tolist()
indy = nonzero_tup[1].tolist()
verts = []
for i, x in enumerate(indx) :
verts.append((x, indy[i]))
for vert in verts :
visited = []
visited.append(vert)
while len(visited) < n_edges + 1 :
try :
ind = indx.index(vert[1])
vert = (indx[ind], indy[ind])
if vert in visited :
return visited
else :
visited.append(vert)
except :
break
return []
# Constraint to not have d14 and d41 simultaneously. Does not prevent circular symmetry with more elements
def break_sym(landmarks, A_ub, b_ub):
L = len(landmarks)
upper_ind = np.triu_indices(L,0,L)
up_ind_x = upper_ind[0]
up_ind_y = upper_ind[1]
for i, _ in enumerate(up_ind_x) :
l = [0]*L*L
if up_ind_x[i] != up_ind_y[i] :
l[up_ind_x[i]*L + up_ind_y[i]] = 1
l[up_ind_y[i]*L + up_ind_x[i]] = 1
A_ub = np.vstack((A_ub,l))
b_ub.append(1)
"""for i in range(7):
print(l[i*7:i*7+7])
print("\n")"""
return A_ub, b_ub
# Constraint to not have circular paths. Want to go from start -> finish without unconnected loops
def break_circle(landmarks, A_ub, b_ub, circle) :
N = len(landmarks)
l = [0]*N*N
for index in circle :
x = index[0]
y = index[1]
l[x*N+y] = 1
A_ub = np.vstack((A_ub,l))
b_ub.append(len(circle)-1)
"""print("\n\nPREVENT CIRCLE")
for i in range(7):
print(l[i*7:i*7+7])
print("\n")"""
return A_ub, b_ub
# Constraint to respect max number of travels
def respect_number(landmarks, A_ub, b_ub):
h = []
for i in range(len(landmarks)) : h.append([1]*len(landmarks))
T = block_diag(*h)
"""for l in T :
for i in range(7):
print(l[i*7:i*7+7])
print("\n")"""
return np.vstack((A_ub, T)), b_ub + [1]*len(landmarks)
# Constraint to tie the problem together. Necessary but not sufficient to avoid circles
def respect_order(landmarks: list, A_eq, b_eq):
N = len(landmarks)
for i in range(N-1) : # Prevent stacked ones
if i == 0 :
continue
else :
l = [0]*N
l[i] = -1
l = l*N
for j in range(N) :
l[i*N + j] = 1
A_eq = np.vstack((A_eq,l))
b_eq.append(0)
"""for i in range(7):
print(l[i*7:i*7+7])
print("\n")"""
return A_eq, b_eq
# Compute manhattan distance between 2 locations
def manhattan_distance(loc1: tuple, loc2: tuple):
x1, y1 = loc1
x2, y2 = loc2
return abs(x1 - x2) + abs(y1 - y2)
# Constraint to not stay in position
def init_eq_not_stay(landmarks):
L = len(landmarks)
l = [0]*L*L
for i in range(L) :
for j in range(L) :
if j == i :
l[j + i*L] = 1
l[L-1] = 1 # cannot skip from start to finish
#A_eq = np.array([np.array(xi) for xi in A_eq]) # Must convert A_eq into an np array
l = np.array(np.array(l))
"""for i in range(7):
print(l[i*7:i*7+7])"""
return [l], [0]
# Initialize A and c. Compute the distances from all landmarks to each other and store attractiveness
# We want to maximize the sightseeing : max(c) st. A*x < b and A_eq*x = b_eq
def init_ub_dist(landmarks: list, max_steps: int):
# Objective function coefficients. a*x1 + b*x2 + c*x3 + ...
c = []
# Coefficients of inequality constraints (left-hand side)
A = []
for i, spot1 in enumerate(landmarks) :
dist_table = [0]*len(landmarks)
c.append(-spot1.attractiveness)
for j, spot2 in enumerate(landmarks) :
dist_table[j] = manhattan_distance(spot1.loc, spot2.loc)
A.append(dist_table)
c = c*len(landmarks)
A_ub = []
for line in A :
#print(line)
A_ub += line
return c, A_ub, [max_steps]
# Go through the landmarks and force the optimizer to use landmarks where attractiveness is set to -1
def respect_user_mustsee(landmarks: list, A_eq: list, b_eq: list) :
L = len(landmarks)
H = 0 # sort of heuristic to get an idea of the number of steps needed
for i in landmarks :
if i.name == "départ" : elem_prev = i # list of all matches
for i, elem in enumerate(landmarks) :
if elem.attractiveness == -1 :
l = [0]*L*L
if elem.name != "arrivée" :
for j in range(L) :
l[j +i*L] = 1
else : # This ensures we go to goal
for k in range(L-1) :
l[k*L+L-1] = 1
H += manhattan_distance(elem.loc, elem_prev.loc)
elem_prev = elem
"""for i in range(7):
print(l[i*7:i*7+7])
print("\n")"""
A_eq = np.vstack((A_eq,l))
b_eq.append(1)
return A_eq, b_eq, H
# Computes the path length given path matrix (dist_table) and a result
def path_length(P: list, resx: list) :
return np.dot(P, resx)
# Main optimization pipeline
def solve_optimization (landmarks, max_steps, printing_details) :
# SET CONSTRAINTS FOR INEQUALITY
c, A_ub, b_ub = init_ub_dist(landmarks, max_steps) # Add the distances from each landmark to the other
P = A_ub # store the paths for later. Needed to compute path length
A_ub, b_ub = respect_number(landmarks, A_ub, b_ub) # Respect max number of visits.
# TODO : Problems with circular symmetry
A_ub, b_ub = break_sym(landmarks, A_ub, b_ub) # break the symmetry. Only use the upper diagonal values
# SET CONSTRAINTS FOR EQUALITY
A_eq, b_eq = init_eq_not_stay(landmarks) # Force solution not to stay in same place
A_eq, b_eq, H = respect_user_mustsee(landmarks, A_eq, b_eq) # Check if there are user_defined must_see. Also takes care of start/goal
A_eq, b_eq = respect_order(landmarks, A_eq, b_eq) # Respect order of visit (only works when max_steps is limiting factor)
# Bounds for variables (x can only be 0 or 1)
x_bounds = [(0, 1)] * len(c)
# Solve linear programming problem
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
# Raise error if no solution is found
if not res.success :
# Override the max_steps using the heuristic
for i, val in enumerate(b_ub) :
if val == max_steps : b_ub[i] = H
# Solve problem again :
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
if not res.success :
s = "No solution could be found, even when increasing max_steps using the heuristic"
return s
#raise ValueError("No solution could be found, even when increasing max_steps using the heuristic")
# If there is a solution, we're good to go, just check for
else :
circle = has_circle(res.x)
i = 0
# Break the circular symmetry if needed
while len(circle) != 0 :
A_ub, b_ub = break_circle(landmarks, A_ub, b_ub, circle)
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
circle = has_circle(res.x)
i += 1
if printing_details is True :
if i != 0 :
print(f"Neded to recompute paths {i} times because of unconnected loops...")
X = print_res(res, landmarks, P)
return X
else :
return untangle(res.x)

View File

@ -0,0 +1,11 @@
'leisure'='park'
geological
'natural'='geyser'
'natural'='hot_spring'
'natural'='arch'
'natural'='volcano'
'natural'='stone'
'tourism'='alpine_hut'
'tourism'='viewpoint'
'tourism'='zoo'
'waterway'='waterfall'

View File

@ -0,0 +1,2 @@
'shop'='department_store'
'shop'='mall'

View File

@ -0,0 +1,8 @@
'tourism'='museum'
'tourism'='attraction'
'tourism'='gallery'
historic
'amenity'='planetarium'
'amenity'='place_of_worship'
'amenity'='fountain'
'water'='reflecting_pool'

View File

@ -0,0 +1,365 @@
import math as m
import json, os
from typing import List, Tuple, Optional
from OSMPythonTools.overpass import Overpass, overpassQueryBuilder
from structs.landmarks import Landmark, LandmarkType
from structs.preferences import Preferences, Preference
SIGHTSEEING = LandmarkType(landmark_type='sightseeing')
NATURE = LandmarkType(landmark_type='nature')
SHOPPING = LandmarkType(landmark_type='shopping')
# Include the json here
# Create a list of all things to visit given some preferences and a city. Ready for the optimizer
def generate_landmarks(preferences: Preferences, coordinates: Tuple[float, float]) :
l_sights, l_nature, l_shop = get_amenities()
L = []
# List for sightseeing
if preferences.sightseeing.score != 0 :
L1 = get_landmarks(l_sights, SIGHTSEEING, coordinates=coordinates)
correct_score(L1, preferences.sightseeing)
L += L1
# List for nature
if preferences.nature.score != 0 :
L2 = get_landmarks(l_nature, NATURE, coordinates=coordinates)
correct_score(L2, preferences.nature)
L += L2
# List for shopping
if preferences.shopping.score != 0 :
L3 = get_landmarks(l_shop, SHOPPING, coordinates=coordinates)
correct_score(L3, preferences.shopping)
L += L3
L = remove_duplicates(L)
return L, take_most_important(L)
"""def generate_landmarks(preferences: Preferences, city_country: str = None, coordinates: Tuple[float, float] = None) -> Tuple[List[Landmark], List[Landmark]] :
l_sights, l_nature, l_shop = get_amenities()
L = []
# List for sightseeing
if preferences.sightseeing.score != 0 :
L1 = get_landmarks(l_sights, SIGHTSEEING, city_country=city_country, coordinates=coordinates)
correct_score(L1, preferences.sightseeing)
L += L1
# List for nature
if preferences.nature.score != 0 :
L2 = get_landmarks(l_nature, NATURE, city_country=city_country, coordinates=coordinates)
correct_score(L2, preferences.nature)
L += L2
# List for shopping
if preferences.shopping.score != 0 :
L3 = get_landmarks(l_shop, SHOPPING, city_country=city_country, coordinates=coordinates)
correct_score(L3, preferences.shopping)
L += L3
return remove_duplicates(L), take_most_important(L)
"""
# Helper function to gather the amenities list
def get_amenities() -> List[List[str]] :
# Get the list of amenities from the files
sightseeing = get_list('/amenities/sightseeing.am')
nature = get_list('/amenities/nature.am')
shopping = get_list('/amenities/shopping.am')
return sightseeing, nature, shopping
# Helper function to read a .am file and generate the corresponding list
def get_list(path: str) -> List[str] :
with open(os.path.dirname(os.path.abspath(__file__)) + path) as f :
content = f.readlines()
amenities = []
for line in content :
amenities.append(line.strip('\n'))
return amenities
# Take the most important landmarks from the list
def take_most_important(L: List[Landmark], N = 0) -> List[Landmark] :
# Read the parameters from the file
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/landmarks_manager.params', "r") as f :
parameters = json.loads(f.read())
N_important = parameters['N important']
L_copy = []
L_clean = []
scores = [0]*len(L)
names = []
name_id = {}
for i, elem in enumerate(L) :
if elem.name not in names :
names.append(elem.name)
name_id[elem.name] = [i]
L_copy.append(elem)
else :
name_id[elem.name] += [i]
scores = []
for j in name_id[elem.name] :
scores.append(L[j].attractiveness)
best_id = max(range(len(scores)), key=scores.__getitem__)
t = name_id[elem.name][best_id]
if t == i :
for old in L_copy :
if old.name == elem.name :
old.attractiveness = L[t].attractiveness
scores = [0]*len(L_copy)
for i, elem in enumerate(L_copy) :
scores[i] = elem.attractiveness
res = sorted(range(len(scores)), key = lambda sub: scores[sub])[-(N_important-N):]
for i, elem in enumerate(L_copy) :
if i in res :
L_clean.append(elem)
return L_clean
# Remove duplicate elements and elements with low score
def remove_duplicates(L: List[Landmark]) -> List[Landmark] :
"""
Removes duplicate landmarks based on their names from the given list.
Parameters:
L (List[Landmark]): A list of Landmark objects.
Returns:
List[Landmark]: A list of unique Landmark objects based on their names.
"""
L_clean = []
names = []
for landmark in L :
if landmark.name in names:
continue
else :
names.append(landmark.name)
L_clean.append(landmark)
return L_clean
# Correct the score of a list of landmarks by taking into account preference settings
def correct_score(L: List[Landmark], preference: Preference) :
if len(L) == 0 :
return
if L[0].type != preference.type :
raise TypeError(f"LandmarkType {preference.type} does not match the type of Landmark {L[0].name}")
for elem in L :
elem.attractiveness = int(elem.attractiveness*preference.score/500) # arbitrary computation
# Function to count elements within a certain radius of a location
def count_elements_within_radius(coordinates: Tuple[float, float], radius: int) -> int:
lat = coordinates[0]
lon = coordinates[1]
alpha = (180*radius)/(6371000*m.pi)
bbox = {'latLower':lat-alpha,'lonLower':lon-alpha,'latHigher':lat+alpha,'lonHigher': lon+alpha}
# Build the query to find elements within the radius
radius_query = overpassQueryBuilder(bbox=[bbox['latLower'],bbox['lonLower'],bbox['latHigher'],bbox['lonHigher']],
elementType=['node', 'way', 'relation'])
try :
overpass = Overpass()
radius_result = overpass.query(radius_query)
return radius_result.countElements()
except :
return None
# Creates a bounding box around given coordinates
def create_bbox(coordinates: Tuple[float, float], side_length: int) -> Tuple[float, float, float, float]:
lat = coordinates[0]
lon = coordinates[1]
# Half the side length in km (since it's a square bbox)
half_side_length_km = side_length / 2.0
# 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 * m.cos(m.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 get_landmarks(list_amenity: list, landmarktype: LandmarkType, coordinates: Tuple[float, float]) -> List[Landmark] :
# Read the parameters from the file
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/landmarks_manager.params', "r") as f :
parameters = json.loads(f.read())
tag_coeff = parameters['tag coeff']
park_coeff = parameters['park coeff']
church_coeff = parameters['church coeff']
radius = parameters['radius close to']
bbox_side = parameters['city bbox side']
# Create bbox around start location
bbox = create_bbox(coordinates, bbox_side)
# Initialize some variables
N = 0
L = []
overpass = Overpass()
for amenity in list_amenity :
query = overpassQueryBuilder(bbox=bbox, elementType=['way', 'relation'], selector=amenity, includeCenter=True, out='body')
result = overpass.query(query)
N += result.countElements()
for elem in result.elements():
name = elem.tag('name') # Add name
location = (elem.centerLat(), elem.centerLon()) # Add coordinates (lat, lon)
# skip if unprecise location
if name is None or location[0] is None:
continue
# skip if unused
if 'disused:leisure' in elem.tags().keys():
continue
# skip if part of another building
if 'building:part' in elem.tags().keys() and elem.tag('building:part') == 'yes':
continue
else :
osm_type = elem.type() # Add type : 'way' or 'relation'
osm_id = elem.id() # Add OSM id
elem_type = landmarktype # Add the landmark type as 'sightseeing
n_tags = len(elem.tags().keys()) # Add number of tags
# Add score of given landmark based on the number of surrounding elements. Penalty for churches as there are A LOT
if amenity == "'amenity'='place_of_worship'" :
score = int((count_elements_within_radius(location, radius) + n_tags*tag_coeff )*church_coeff)
elif amenity == "'leisure'='park'" :
score = int((count_elements_within_radius(location, radius) + n_tags*tag_coeff )*park_coeff)
else :
score = count_elements_within_radius(location, radius) + n_tags*tag_coeff
if score is not None :
# Generate the landmark and append it to the list
landmark = Landmark(name=name, type=elem_type, location=location, osm_type=osm_type, osm_id=osm_id, attractiveness=score, must_do=False, n_tags=n_tags)
L.append(landmark)
return L
"""def get_landmarks(list_amenity: list, landmarktype: LandmarkType, city_country: str = None, coordinates: Tuple[float, float] = None) -> List[Landmark] :
if city_country is None and coordinates is None :
raise ValueError("Either one of 'city_country' and 'coordinates' arguments must be specified")
if city_country is not None and coordinates is not None :
raise ValueError("Cannot specify both 'city_country' and 'coordinates' at the same time, please choose either one")
# Read the parameters from the file
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/landmarks_manager.params', "r") as f :
parameters = json.loads(f.read())
tag_coeff = parameters['tag coeff']
park_coeff = parameters['park coeff']
church_coeff = parameters['church coeff']
radius = parameters['radius close to']
bbox_side = parameters['city bbox side']
# If city_country is specified :
if city_country is not None :
nominatim = Nominatim()
areaId = nominatim.query(city_country).areaId()
bbox = None
# If coordinates are specified :
elif coordinates is not None :
bbox = create_bbox(coordinates, bbox_side)
areaId = None
else :
raise ValueError("Argument number is not corresponding.")
# Initialize some variables
N = 0
L = []
overpass = Overpass()
for amenity in list_amenity :
query = overpassQueryBuilder(area=areaId, bbox=bbox, elementType=['way', 'relation'], selector=amenity, includeCenter=True, out='body')
result = overpass.query(query)
N += result.countElements()
for elem in result.elements():
name = elem.tag('name') # Add name
location = (elem.centerLat(), elem.centerLon()) # Add coordinates (lat, lon)
# skip if unprecise location
if name is None or location[0] is None:
continue
# skip if unused
if 'disused:leisure' in elem.tags().keys():
continue
# skip if part of another building
if 'building:part' in elem.tags().keys() and elem.tag('building:part') == 'yes':
continue
else :
osm_type = elem.type() # Add type : 'way' or 'relation'
osm_id = elem.id() # Add OSM id
elem_type = landmarktype # Add the landmark type as 'sightseeing
n_tags = len(elem.tags().keys()) # Add number of tags
# Add score of given landmark based on the number of surrounding elements. Penalty for churches as there are A LOT
if amenity == "'amenity'='place_of_worship'" :
score = int((count_elements_within_radius(location, radius) + n_tags*tag_coeff )*church_coeff)
elif amenity == "'leisure'='park'" :
score = int((count_elements_within_radius(location, radius) + n_tags*tag_coeff )*park_coeff)
else :
score = count_elements_within_radius(location, radius) + n_tags*tag_coeff
if score is not None :
# Generate the landmark and append it to the list
landmark = Landmark(name=name, type=elem_type, location=location, osm_type=osm_type, osm_id=osm_id, attractiveness=score, must_do=False, n_tags=n_tags)
L.append(landmark)
return L
"""

70
backend/src/main.py Normal file
View File

@ -0,0 +1,70 @@
from optimizer import solve_optimization
from refiner import refine_optimization
from landmarks_manager import generate_landmarks
from structs.landmarks import Landmark
from structs.landmarktype import LandmarkType
from structs.preferences import Preferences, Preference
from fastapi import FastAPI, Query, Body
from typing import List
app = FastAPI()
# Assuming frontend is calling like this :
#"http://127.0.0.1:8000/process?param1={param1}&param2={param2}"
@app.post("/optimizer_coords/{start_lat}/{start_lon}/{finish_lat}/{finish_lon}")
def main1(start_lat: float, start_lon: float, preferences: Preferences = Body(...), finish_lat: float = None, finish_lon: float = None) -> List[Landmark]:
if preferences is None :
raise ValueError("Please provide preferences in the form of a 'Preference' BaseModel class.")
if bool(start_lat) ^ bool(start_lon) :
raise ValueError("Please provide both latitude and longitude for the starting point")
if bool(finish_lat) ^ bool(finish_lon) :
raise ValueError("Please provide both latitude and longitude for the finish point")
start = Landmark(name='start', type=LandmarkType(landmark_type='start'), location=(start_lat, start_lon), osm_type='start', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
if bool(finish_lat) and bool(finish_lon) :
finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(finish_lat, finish_lon), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
else :
finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(start_lat, start_lon), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
start = Landmark(name='start', type=LandmarkType(landmark_type='start'), location=(48.8375946, 2.2949904), osm_type='start', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(48.8375946, 2.2949904), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
# Generate the landmarks from the start location
landmarks, landmarks_short = generate_landmarks(preferences=preferences, coordinates=start.location)
# insert start and finish to the landmarks list
landmarks_short.insert(0, start)
landmarks_short.append(finish)
# TODO use these parameters in another way
max_walking_time = 4 # hours
detour = 30 # minutes
# First stage optimization
base_tour = solve_optimization(landmarks_short, max_walking_time*60, True)
# Second stage optimization
refined_tour = refine_optimization(landmarks, base_tour, max_walking_time*60+detour, True)
return refined_tour
# input city, country in the form of 'Paris, France'
@app.post("/test2/{city_country}")
def test2(city_country: str, preferences: Preferences = Body(...)) -> List[Landmark]:
landmarks = generate_landmarks(city_country, preferences)
max_steps = 9000000
visiting_order = solve_optimization(landmarks, max_steps, True)

411
backend/src/optimizer.py Normal file
View File

@ -0,0 +1,411 @@
import numpy as np
import json, os
from typing import List, Tuple
from scipy.optimize import linprog
from math import radians, sin, cos, acos
from shapely import Polygon
from structs.landmarks import Landmark
# Function to print the result
def print_res(L: List[Landmark], L_tot):
if len(L) == L_tot:
print('\nAll landmarks can be visited within max_steps, the following order is suggested : ')
else :
print('Could not visit all the landmarks, the following order is suggested : ')
dist = 0
for elem in L :
if elem.name != 'start' :
print('- ' + elem.name + ', time to reach = ' + str(elem.time_to_reach))
dist += elem.time_to_reach
else :
print('- ' + elem.name)
print("\nMinutes walked : " + str(dist))
print(f"Visited {len(L)-2} out of {L_tot-2} landmarks")
# Prevent the use of a particular solution
def prevent_config(resx, A_ub, b_ub) -> bool:
for i, elem in enumerate(resx):
resx[i] = round(elem)
N = len(resx) # Number of edges
L = int(np.sqrt(N)) # Number of landmarks
nonzeroind = np.nonzero(resx)[0] # the return is a little funky so I use the [0]
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
ind_a = nonzero_tup[0].tolist()
vertices_visited = ind_a
vertices_visited.remove(0)
ones = [1]*L
h = [0]*N
for i in range(L) :
if i in vertices_visited :
h[i*L:i*L+L] = ones
A_ub = np.vstack((A_ub, h))
b_ub.append(len(vertices_visited)-1)
return A_ub, b_ub
# Prevent the possibility of a given solution bit
def break_cricle(circle_vertices: list, L: int, A_ub: list, b_ub: list) -> bool:
if L-1 in circle_vertices :
circle_vertices.remove(L-1)
h = [0]*L*L
for i in range(L) :
if i in circle_vertices :
h[i*L:i*L+L] = [1]*L
A_ub = np.vstack((A_ub, h))
b_ub.append(len(circle_vertices)-1)
return A_ub, b_ub
# Checks if the path is connected, returns a circle if it finds one and the RESULT
def is_connected(resx) -> bool:
# first round the results to have only 0-1 values
for i, elem in enumerate(resx):
resx[i] = round(elem)
N = len(resx) # length of res
L = int(np.sqrt(N)) # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
n_edges = resx.sum() # number of edges
nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0]
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
ind_a = nonzero_tup[0].tolist()
ind_b = nonzero_tup[1].tolist()
edges = []
edges_visited = []
vertices_visited = []
edge1 = (ind_a[0], ind_b[0])
edges_visited.append(edge1)
vertices_visited.append(edge1[0])
for i, a in enumerate(ind_a) :
edges.append((a, ind_b[i])) # Create the list of edges
remaining = edges
remaining.remove(edge1)
break_flag = False
while len(remaining) > 0 and not break_flag:
for edge2 in remaining :
if edge2[0] == edge1[1] :
if edge1[1] in vertices_visited :
edges_visited.append(edge2)
break_flag = True
break
else :
vertices_visited.append(edge1[1])
edges_visited.append(edge2)
remaining.remove(edge2)
edge1 = edge2
elif edge1[1] == L-1 or edge1[1] in vertices_visited:
break_flag = True
break
vertices_visited.append(edge1[1])
if len(vertices_visited) == n_edges +1 :
return vertices_visited, []
else:
return vertices_visited, edges_visited
# Function that returns the distance in meters from one location to another
def get_distance(p1: Tuple[float, float], p2: Tuple[float, float], detour: float, speed: float) :
# Compute the straight-line distance in km
if p1 == p2 :
return 0, 0
else:
dist = 6371.01 * acos(sin(radians(p1[0]))*sin(radians(p2[0])) + cos(radians(p1[0]))*cos(radians(p2[0]))*cos(radians(p1[1]) - radians(p2[1])))
# Consider the detour factor for average cityto deterline walking distance (in km)
walk_dist = dist*detour
# Time to walk this distance (in minutes)
walk_time = walk_dist/speed*60
if walk_time > 15 :
walk_time = 5*round(walk_time/5)
else :
walk_time = round(walk_time)
return round(walk_dist, 1), walk_time
# Initialize A and c. Compute the distances from all landmarks to each other and store attractiveness
# We want to maximize the sightseeing : max(c) st. A*x < b and A_eq*x = b_eq
def init_ub_dist(landmarks: List[Landmark], max_steps: int):
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f :
parameters = json.loads(f.read())
detour = parameters['detour factor']
speed = parameters['average walking speed']
# Objective function coefficients. a*x1 + b*x2 + c*x3 + ...
c = []
# Coefficients of inequality constraints (left-hand side)
A_ub = []
for spot1 in landmarks :
dist_table = [0]*len(landmarks)
c.append(-spot1.attractiveness)
for j, spot2 in enumerate(landmarks) :
t = get_distance(spot1.location, spot2.location, detour, speed)[1]
dist_table[j] = t
A_ub += dist_table
c = c*len(landmarks)
return c, A_ub, [max_steps]
# Constraint to respect only one travel per landmark. Also caps the total number of visited landmarks
def respect_number(L:int, A_ub, b_ub):
ones = [1]*L
zeros = [0]*L
for i in range(L) :
h = zeros*i + ones + zeros*(L-1-i)
A_ub = np.vstack((A_ub, h))
b_ub.append(1)
# Read the parameters from the file
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f :
parameters = json.loads(f.read())
max_landmarks = parameters['max landmarks']
A_ub = np.vstack((A_ub, ones*L))
b_ub.append(max_landmarks+1)
return A_ub, b_ub
# Constraint to not have d14 and d41 simultaneously. Does not prevent circular symmetry with more elements
def break_sym(L, A_ub, b_ub):
upper_ind = np.triu_indices(L,0,L)
up_ind_x = upper_ind[0]
up_ind_y = upper_ind[1]
for i, _ in enumerate(up_ind_x) :
l = [0]*L*L
if up_ind_x[i] != up_ind_y[i] :
l[up_ind_x[i]*L + up_ind_y[i]] = 1
l[up_ind_y[i]*L + up_ind_x[i]] = 1
A_ub = np.vstack((A_ub,l))
b_ub.append(1)
return A_ub, b_ub
# Constraint to not stay in position. Removes d11, d22, d33, etc.
def init_eq_not_stay(L: int):
l = [0]*L*L
for i in range(L) :
for j in range(L) :
if j == i :
l[j + i*L] = 1
l = np.array(np.array(l))
return [l], [0]
# Go through the landmarks and force the optimizer to use landmarks where attractiveness is set to -1
def respect_user_mustsee(landmarks: List[Landmark], A_eq: list, b_eq: list) :
L = len(landmarks)
for i, elem in enumerate(landmarks) :
if elem.must_do is True and elem.name not in ['finish', 'start']:
l = [0]*L*L
for j in range(L) : # sets the horizontal ones (go from)
l[j +i*L] = 1 # sets the vertical ones (go to) double check if good
for k in range(L-1) :
l[k*L+L-1] = 1
A_eq = np.vstack((A_eq,l))
b_eq.append(2)
return A_eq, b_eq
# Constraint to ensure start at start and finish at goal
def respect_start_finish(L: int, A_eq: list, b_eq: list):
ls = [1]*L + [0]*L*(L-1) # sets only horizontal ones for start (go from)
ljump = [0]*L*L
ljump[L-1] = 1 # Prevent start finish jump
lg = [0]*L*L
ll = [0]*L*(L-1) + [1]*L
for k in range(L-1) : # sets only vertical ones for goal (go to)
ll[k*L] = 1
if k != 0 : # Prevent the shortcut start -> finish
lg[k*L+L-1] = 1
A_eq = np.vstack((A_eq,ls))
A_eq = np.vstack((A_eq,ljump))
A_eq = np.vstack((A_eq,lg))
A_eq = np.vstack((A_eq,ll))
b_eq.append(1)
b_eq.append(0)
b_eq.append(1)
b_eq.append(0)
return A_eq, b_eq
# Constraint to tie the problem together. Necessary but not sufficient to avoid circles
def respect_order(N: int, A_eq, b_eq):
for i in range(N-1) : # Prevent stacked ones
if i == 0 or i == N-1: # Don't touch start or finish
continue
else :
l = [0]*N
l[i] = -1
l = l*N
for j in range(N) :
l[i*N + j] = 1
A_eq = np.vstack((A_eq,l))
b_eq.append(0)
return A_eq, b_eq
# Computes the time to reach from each landmark to the next
def add_time_to_reach(order: List[int], landmarks: List[Landmark])->List[Landmark] :
# Read the parameters from the file
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f :
parameters = json.loads(f.read())
detour_factor = parameters['detour factor']
speed = parameters['average walking speed']
j = 0
L = []
prev = landmarks[0]
while(len(L) != len(order)) :
elem = landmarks[order[j]]
if elem != prev :
elem.time_to_reach = get_distance(elem.location, prev.location, detour_factor, speed)[1]
elem.must_do = True
L.append(elem)
prev = elem
j += 1
return L
def add_time_to_reach_simple(ordered_visit: List[Landmark])-> List[Landmark] :
# Read the parameters from the file
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f :
parameters = json.loads(f.read())
detour_factor = parameters['detour factor']
speed = parameters['average walking speed']
L = []
prev = ordered_visit[0]
L.append(prev)
total_dist = 0
for elem in ordered_visit[1:] :
elem.time_to_reach = get_distance(elem.location, prev.location, detour_factor, speed)[1]
elem.must_do = True
L.append(elem)
prev = elem
total_dist += get_distance(elem.location, prev.location, detour_factor, speed)[1]
return L, total_dist
# Main optimization pipeline
def solve_optimization (landmarks :List[Landmark], max_steps: int, printing_details: bool) :
L = len(landmarks)
# SET CONSTRAINTS FOR INEQUALITY
c, A_ub, b_ub = init_ub_dist(landmarks, max_steps) # Add the distances from each landmark to the other
A_ub, b_ub = respect_number(L, A_ub, b_ub) # Respect max number of visits (no more possible stops than landmarks).
A_ub, b_ub = break_sym(L, A_ub, b_ub) # break the 'zig-zag' symmetry
# SET CONSTRAINTS FOR EQUALITY
A_eq, b_eq = init_eq_not_stay(L) # Force solution not to stay in same place
A_eq, b_eq = respect_user_mustsee(landmarks, A_eq, b_eq) # Check if there are user_defined must_see. Also takes care of start/goal
A_eq, b_eq = respect_start_finish(L, A_eq, b_eq) # Force start and finish positions
A_eq, b_eq = respect_order(L, A_eq, b_eq) # Respect order of visit (only works when max_steps is limiting factor)
# SET BOUNDS FOR DECISION VARIABLE (x can only be 0 or 1)
x_bounds = [(0, 1)]*L*L
# Solve linear programming problem
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
# Raise error if no solution is found
if not res.success :
raise ArithmeticError("No solution could be found, the problem is overconstrained. Please adapt your must_dos")
# If there is a solution, we're good to go, just check for connectiveness
else :
order, circle = is_connected(res.x)
i = 0
timeout = 80
while len(circle) != 0 and i < timeout:
A_ub, b_ub = prevent_config(res.x, A_ub, b_ub)
A_ub, b_ub = break_cricle(order, len(landmarks), A_ub, b_ub)
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
order, circle = is_connected(res.x)
if len(circle) == 0 :
break
print(i)
i += 1
if i == timeout :
raise TimeoutError(f"Optimization took too long. No solution found after {timeout} iterations.")
# Add the times to reach and stop optimizing
L = add_time_to_reach(order, landmarks)
if printing_details is True :
if i != 0 :
print(f"Neded to recompute paths {i} times because of unconnected loops...")
print_res(L, len(landmarks))
print("\nTotal score : " + str(int(-res.fun)))
return L

View File

@ -0,0 +1,8 @@
{
"city bbox side" : 10,
"radius close to" : 27.5,
"church coeff" : 0.6,
"park coeff" : 1.5,
"tag coeff" : 100,
"N important" : 40
}

View File

@ -0,0 +1,5 @@
{
"detour factor" : 1.4,
"average walking speed" : 4.8,
"max landmarks" : 10
}

293
backend/src/refiner.py Normal file
View File

@ -0,0 +1,293 @@
from collections import defaultdict
from heapq import heappop, heappush
from itertools import permutations
import os, json
from shapely import buffer, LineString, Point, Polygon, MultiPoint, convex_hull, concave_hull, LinearRing
from typing import List, Tuple
from math import pi
from structs.landmarks import Landmark
from landmarks_manager import take_most_important
from optimizer import solve_optimization, add_time_to_reach_simple, print_res, get_distance
def create_corridor(landmarks: List[Landmark], width: float) :
corrected_width = (180*width)/(6371000*pi)
path = create_linestring(landmarks)
obj = buffer(path, corrected_width, join_style="mitre", cap_style="square", mitre_limit=2)
return obj
def create_linestring(landmarks: List[Landmark])->List[Point] :
points = []
for landmark in landmarks :
points.append(Point(landmark.location))
return LineString(points)
def is_in_area(area: Polygon, coordinates) -> bool :
point = Point(coordinates)
return point.within(area)
def is_close_to(location1: Tuple[float], location2: Tuple[float]):
"""Determine if two locations are close by rounding their coordinates to 3 decimals."""
absx = abs(location1[0] - location2[0])
absy = abs(location1[1] - location2[1])
return absx < 0.001 and absy < 0.001
#return (round(location1[0], 3), round(location1[1], 3)) == (round(location2[0], 3), round(location2[1], 3))
def rearrange(landmarks: List[Landmark]) -> List[Landmark]:
i = 1
while i < len(landmarks):
j = i+1
while j < len(landmarks):
if is_close_to(landmarks[i].location, landmarks[j].location) and landmarks[i].name not in ['start', 'finish'] and landmarks[j].name not in ['start', 'finish']:
# If they are not adjacent, move the j-th element to be adjacent to the i-th element
if j != i + 1:
landmarks.insert(i + 1, landmarks.pop(j))
break # Move to the next i-th element after rearrangement
j += 1
i += 1
return landmarks
"""
def find_shortest_path(landmarks: List[Landmark]) -> List[Landmark]:
# Read from data
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f :
parameters = json.loads(f.read())
detour = parameters['detour factor']
speed = parameters['average walking speed']
# Step 1: Build the graph
graph = defaultdict(list)
for i in range(len(landmarks)):
for j in range(len(landmarks)):
if i != j:
distance = get_distance(landmarks[i].location, landmarks[j].location, detour, speed)[1]
graph[i].append((distance, j))
# Step 2: Dijkstra's algorithm to find the shortest path from start to finish
start_idx = next(i for i, lm in enumerate(landmarks) if lm.name == 'start')
finish_idx = next(i for i, lm in enumerate(landmarks) if lm.name == 'finish')
distances = {i: float('inf') for i in range(len(landmarks))}
previous_nodes = {i: None for i in range(len(landmarks))}
distances[start_idx] = 0
priority_queue = [(0, start_idx)]
while priority_queue:
current_distance, current_index = heappop(priority_queue)
if current_distance > distances[current_index]:
continue
for neighbor_distance, neighbor_index in graph[current_index]:
distance = current_distance + neighbor_distance
if distance < distances[neighbor_index]:
distances[neighbor_index] = distance
previous_nodes[neighbor_index] = current_index
heappush(priority_queue, (distance, neighbor_index))
# Step 3: Backtrack from finish to start to find the path
path = []
current_index = finish_idx
while current_index is not None:
path.append(landmarks[current_index])
current_index = previous_nodes[current_index]
path.reverse()
return path
"""
"""
def total_path_distance(path: List[Landmark], detour, speed) -> float:
total_distance = 0
for i in range(len(path) - 1):
total_distance += get_distance(path[i].location, path[i + 1].location, detour, speed)[1]
return total_distance
"""
def find_shortest_path_through_all_landmarks(landmarks: List[Landmark]) -> List[Landmark]:
# Read from data
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f :
parameters = json.loads(f.read())
detour = parameters['detour factor']
speed = parameters['average walking speed']
# Step 1: Find 'start' and 'finish' landmarks
start_idx = next(i for i, lm in enumerate(landmarks) if lm.name == 'start')
finish_idx = next(i for i, lm in enumerate(landmarks) if lm.name == 'finish')
start_landmark = landmarks[start_idx]
finish_landmark = landmarks[finish_idx]
# Step 2: Create a list of unvisited landmarks excluding 'start' and 'finish'
unvisited_landmarks = [lm for i, lm in enumerate(landmarks) if i not in [start_idx, finish_idx]]
# Step 3: Initialize the path with the 'start' landmark
path = [start_landmark]
coordinates = [landmarks[start_idx].location]
current_landmark = start_landmark
# Step 4: Use nearest neighbor heuristic to visit all landmarks
while unvisited_landmarks:
nearest_landmark = min(unvisited_landmarks, key=lambda lm: get_distance(current_landmark.location, lm.location, detour, speed)[1])
path.append(nearest_landmark)
coordinates.append(nearest_landmark.location)
current_landmark = nearest_landmark
unvisited_landmarks.remove(nearest_landmark)
# Step 5: Finally add the 'finish' landmark to the path
path.append(finish_landmark)
coordinates.append(landmarks[finish_idx].location)
path_poly = Polygon(coordinates)
return path, path_poly
def get_minor_landmarks(all_landmarks: List[Landmark], visited_landmarks: List[Landmark], width: float) -> List[Landmark] :
second_order_landmarks = []
visited_names = []
area = create_corridor(visited_landmarks, width)
for visited in visited_landmarks :
visited_names.append(visited.name)
for landmark in all_landmarks :
if is_in_area(area, landmark.location) and landmark.name not in visited_names:
second_order_landmarks.append(landmark)
return take_most_important(second_order_landmarks, len(visited_landmarks))
"""def refine_optimization(landmarks: List[Landmark], base_tour: List[Landmark], max_time: int, print_infos: bool) -> List[Landmark] :
minor_landmarks = get_minor_landmarks(landmarks, base_tour, 200)
if print_infos : print("There are " + str(len(minor_landmarks)) + " minor landmarks around the predicted path")
full_set = base_tour[:-1] + minor_landmarks # create full set of possible landmarks (without finish)
full_set.append(base_tour[-1]) # add finish back
new_tour = solve_optimization(full_set, max_time, print_infos)
return new_tour"""
def refine_optimization(landmarks: List[Landmark], base_tour: List[Landmark], max_time: int, print_infos: bool) -> List[Landmark] :
# Read from the file
with open (os.path.dirname(os.path.abspath(__file__)) + '/parameters/optimizer.params', "r") as f :
parameters = json.loads(f.read())
max_landmarks = parameters['max landmarks']
if len(base_tour)-2 >= max_landmarks :
return base_tour
minor_landmarks = get_minor_landmarks(landmarks, base_tour, 200)
if print_infos : print("Using " + str(len(minor_landmarks)) + " minor landmarks around the predicted path")
# full set of visitable landmarks
full_set = base_tour[:-1] + minor_landmarks # create full set of possible landmarks (without finish)
full_set.append(base_tour[-1]) # add finish back
# get a new tour
new_tour = solve_optimization(full_set, max_time, False)
new_tour, new_dist = add_time_to_reach_simple(new_tour)
"""#if base_tour[0].location == base_tour[-1].location :
if False :
coords = [] # Coordinates of the new tour
coords_dict = {} # maps the location of an element to the element itself. Used to access the elements back once we get the geometry
# Iterate through the new tour without finish
for elem in new_tour[:-1] :
coords.append(Point(elem.location))
coords_dict[elem.location] = elem # if start = goal, only finish remains
# Create a concave polygon using the coordinates
better_tour_poly = concave_hull(MultiPoint(coords)) # Create concave hull with "core" of tour leaving out start and finish
xs, ys = better_tour_poly.exterior.xy
# reverse the xs and ys
xs.reverse()
ys.reverse()
better_tour = [] # List of ordered visit
name_index = {} # Maps the name of a landmark to its index in the concave polygon
# Loop through the polygon and generate the better (ordered) tour
for i,x in enumerate(xs[:-1]) :
better_tour.append(coords_dict[tuple((x,ys[i]))])
name_index[coords_dict[tuple((x,ys[i]))].name] = i
# Scroll the list to have start in front again
start_index = name_index['start']
better_tour = better_tour[start_index:] + better_tour[:start_index]
# Append the finish back and correct the time to reach
better_tour.append(new_tour[-1])
# Rearrange only if polygon
better_tour = rearrange(better_tour)
# Add the time to reach
better_tour = add_time_to_reach_simple(better_tour)
"""
"""
if not better_poly.is_simple :
coords_dict = {}
better_tour2 = []
for elem in better_tour :
coords_dict[elem.location] = elem
better_poly2 = better_poly.buffer(0)
new_coords = better_poly2.exterior.coords[:]
start_coords = base_tour[0].location
start_index = new_coords.
#for point in new_coords :
"""
better_tour, better_poly = find_shortest_path_through_all_landmarks(new_tour)
better_tour, better_dist = add_time_to_reach_simple(better_tour)
if new_dist < better_dist :
final_tour = new_tour
else :
final_tour = better_tour
if print_infos :
print("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n")
print("\nRefined tour (result of second stage optimization): ")
print_res(final_tour, len(full_set))
return final_tour

View File

@ -0,0 +1,20 @@
from typing import Optional
from pydantic import BaseModel
from .landmarktype import LandmarkType
# Output to frontend
class Landmark(BaseModel) :
name : str
type: LandmarkType # De facto mapping depending on how the query was executed with overpass. Should still EXACTLY correspond to the preferences
location : tuple
osm_type : str
osm_id : int
attractiveness : int
must_do : bool
n_tags : int
time_to_reach : Optional[int] = 0

View File

@ -0,0 +1,4 @@
from pydantic import BaseModel
class LandmarkType(BaseModel):
landmark_type: str

View File

@ -0,0 +1,28 @@
from pydantic import BaseModel
from .landmarktype import LandmarkType
class Preference(BaseModel) :
name: str
type: LandmarkType # should match the attributes of the Preferences class
score: int # score could be from 1 to 5
# Input for optimization
class Preferences(BaseModel) :
# Sightseeing / History & Culture (Musées, bâtiments historiques, opéras, églises)
sightseeing : Preference
# Nature (parcs, jardins, rivières, plages)
nature: Preference
# Shopping (diriger plutôt vers des zones / rues commerçantes)
shopping : Preference
""" # Food (price low or high. Combien on veut dépenser pour manger à midi/soir)
food_budget : Preference
# Tolérance au détour (ce qui détermine (+ ou -) le chemin emprunté)
detour_tol : Preference"""

116
backend/src/tester.py Normal file
View File

@ -0,0 +1,116 @@
import pandas as pd
from typing import List
from landmarks_manager import generate_landmarks
from fastapi.encoders import jsonable_encoder
from optimizer import solve_optimization
from refiner import refine_optimization
from structs.landmarks import Landmark
from structs.landmarktype import LandmarkType
from structs.preferences import Preferences, Preference
# Helper function to create a .txt file with results
def write_data(L: List[Landmark], file_name: str):
data = pd.DataFrame()
i = 0
for landmark in L :
data[i] = jsonable_encoder(landmark)
i += 1
data.to_json(file_name, indent = 2, force_ascii=False)
def test3(city_country: str) -> List[Landmark]:
preferences = Preferences(
sightseeing=Preference(
name='sightseeing',
type=LandmarkType(landmark_type='sightseeing'),
score = 5),
nature=Preference(
name='nature',
type=LandmarkType(landmark_type='nature'),
score = 0),
shopping=Preference(
name='shopping',
type=LandmarkType(landmark_type='shopping'),
score = 5))
coordinates = None
landmarks, landmarks_short = generate_landmarks(preferences=preferences, city_country=city_country, coordinates=coordinates)
#write_data(landmarks)
start = Landmark(name='start', type=LandmarkType(landmark_type='start'), location=(48.2044576, 16.3870242), osm_type='start', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(48.2044576, 16.3870242), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
test = landmarks_short
test.insert(0, start)
test.append(finish)
max_walking_time = 2 # hours
visiting_list = solve_optimization(test, max_walking_time*60, True)
def test4(coordinates: tuple[float, float]) -> List[Landmark]:
preferences = Preferences(
sightseeing=Preference(
name='sightseeing',
type=LandmarkType(landmark_type='sightseeing'),
score = 5),
nature=Preference(
name='nature',
type=LandmarkType(landmark_type='nature'),
score = 5),
shopping=Preference(
name='shopping',
type=LandmarkType(landmark_type='shopping'),
score = 5))
# Create start and finish
start = Landmark(name='start', type=LandmarkType(landmark_type='start'), location=coordinates, osm_type='start', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=coordinates, osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
#finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(48.8777055, 2.3640967), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
#start = Landmark(name='start', type=LandmarkType(landmark_type='start'), location=(48.847132, 2.312359), osm_type='start', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
#finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(48.843185, 2.344533), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
#finish = Landmark(name='finish', type=LandmarkType(landmark_type='finish'), location=(48.847132, 2.312359), osm_type='finish', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
# Generate the landmarks from the start location
landmarks, landmarks_short = generate_landmarks(preferences=preferences, coordinates=start.location)
#write_data(landmarks, "landmarks.txt")
# Insert start and finish to the landmarks list
landmarks_short.insert(0, start)
landmarks_short.append(finish)
# TODO use these parameters in another way
max_walking_time = 2 # hours
detour = 30 # minutes
# First stage optimization
base_tour = solve_optimization(landmarks_short, max_walking_time*60, True)
# Second stage optimization
refined_tour = refine_optimization(landmarks, base_tour, max_walking_time*60+detour, True)
return refined_tour
#test4(tuple((48.8344400, 2.3220540))) # Café Chez César
#test4(tuple((48.8375946, 2.2949904))) # Point random
test4(tuple((47.377859, 8.540585))) # Zurich HB
#test3('Vienna, Austria')

9730
landmarks.txt Normal file

File diff suppressed because it is too large Load Diff

482
minor_landmarks.txt Normal file
View File

@ -0,0 +1,482 @@
{
"0":{
"name":"Atelier Brancusi",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.8614029,
2.3519903
],
"osm_type":"way",
"osm_id":55503399,
"attractiveness":13,
"must_do":false,
"n_tags":13,
"time_to_reach":0
},
"1":{
"name":"Musée de Cluny",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.8506604,
2.3437398
],
"osm_type":"way",
"osm_id":56640163,
"attractiveness":18,
"must_do":false,
"n_tags":18,
"time_to_reach":0
},
"2":{
"name":"Musée du Luxembourg",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.8485965,
2.3340156
],
"osm_type":"way",
"osm_id":170226810,
"attractiveness":15,
"must_do":false,
"n_tags":15,
"time_to_reach":0
},
"3":{
"name":"Musée national Eugène Delacroix",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.8546662,
2.3353836
],
"osm_type":"way",
"osm_id":395785603,
"attractiveness":15,
"must_do":false,
"n_tags":15,
"time_to_reach":0
},
"4":{
"name":"Hôtel de Sully",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.854635,
2.3638126
],
"osm_type":"relation",
"osm_id":403146,
"attractiveness":14,
"must_do":false,
"n_tags":13,
"time_to_reach":0
},
"5":{
"name":"Hôtel de la Monnaie",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.856403,
2.3388625
],
"osm_type":"relation",
"osm_id":967664,
"attractiveness":11,
"must_do":false,
"n_tags":11,
"time_to_reach":0
},
"6":{
"name":"Musée Bourdelle",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.8432078,
2.3186583
],
"osm_type":"relation",
"osm_id":1212876,
"attractiveness":23,
"must_do":false,
"n_tags":23,
"time_to_reach":0
},
"7":{
"name":"Musée Carnavalet",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.8576819,
2.3627147
],
"osm_type":"relation",
"osm_id":2405955,
"attractiveness":25,
"must_do":false,
"n_tags":25,
"time_to_reach":0
},
"8":{
"name":"Pont Neuf",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.8565248,
2.3408132
],
"osm_type":"way",
"osm_id":53574149,
"attractiveness":16,
"must_do":false,
"n_tags":15,
"time_to_reach":0
},
"9":{
"name":"Jardin du Luxembourg",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.8467137,
2.3363649
],
"osm_type":"way",
"osm_id":128206209,
"attractiveness":35,
"must_do":false,
"n_tags":23,
"time_to_reach":0
},
"10":{
"name":"Cathédrale Notre-Dame de Paris",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.8529372,
2.3498701
],
"osm_type":"way",
"osm_id":201611261,
"attractiveness":55,
"must_do":false,
"n_tags":54,
"time_to_reach":0
},
"11":{
"name":"Maison à l'enseigne du Faucheur",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.8558553,
2.3568266
],
"osm_type":"way",
"osm_id":1123456865,
"attractiveness":13,
"must_do":false,
"n_tags":11,
"time_to_reach":0
},
"12":{
"name":"Maison à l'enseigne du Mouton",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.8558431,
2.3568914
],
"osm_type":"way",
"osm_id":1123456866,
"attractiveness":13,
"must_do":false,
"n_tags":11,
"time_to_reach":0
},
"13":{
"name":"Palais de Justice de Paris",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.8556537,
2.3446072
],
"osm_type":"relation",
"osm_id":536982,
"attractiveness":24,
"must_do":false,
"n_tags":24,
"time_to_reach":0
},
"14":{
"name":"Mémorial des Martyrs de la Déportation",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.8517365,
2.3524734
],
"osm_type":"relation",
"osm_id":9396191,
"attractiveness":21,
"must_do":false,
"n_tags":21,
"time_to_reach":0
},
"15":{
"name":"Fontaine des Innocents",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.8606368,
2.3480233
],
"osm_type":"way",
"osm_id":52469222,
"attractiveness":16,
"must_do":false,
"n_tags":15,
"time_to_reach":0
},
"16":{
"name":"Hôtel de Sens",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.8535257,
2.3588733
],
"osm_type":"way",
"osm_id":55541122,
"attractiveness":21,
"must_do":false,
"n_tags":20,
"time_to_reach":0
},
"17":{
"name":"Hôtel d'Ourscamp",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.8555465,
2.3571206
],
"osm_type":"way",
"osm_id":55620201,
"attractiveness":10,
"must_do":false,
"n_tags":9,
"time_to_reach":0
},
"18":{
"name":"Hôtel de Choiseul-Praslin",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.8478008,
2.3203873
],
"osm_type":"way",
"osm_id":65756922,
"attractiveness":12,
"must_do":false,
"n_tags":12,
"time_to_reach":0
},
"19":{
"name":"Chapelle Notre-Dame-des-Anges",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.8462836,
2.3235558
],
"osm_type":"way",
"osm_id":219378497,
"attractiveness":16,
"must_do":false,
"n_tags":16,
"time_to_reach":0
},
"20":{
"name":"Fontaine du Palmier",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.8575005,
2.3472864
],
"osm_type":"way",
"osm_id":261092850,
"attractiveness":23,
"must_do":false,
"n_tags":14,
"time_to_reach":0
},
"21":{
"name":"Église Saint-Séverin",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.8520913,
2.3457237
],
"osm_type":"way",
"osm_id":19740659,
"attractiveness":15,
"must_do":false,
"n_tags":25,
"time_to_reach":0
},
"22":{
"name":"Église Orthodoxe Roumaine des Saints Archanges",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.849488,
2.3471975
],
"osm_type":"way",
"osm_id":55366403,
"attractiveness":10,
"must_do":false,
"n_tags":16,
"time_to_reach":0
},
"23":{
"name":"Église Saint-Merri",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.8590777,
2.3508448
],
"osm_type":"way",
"osm_id":55742120,
"attractiveness":16,
"must_do":false,
"n_tags":26,
"time_to_reach":0
},
"24":{
"name":"Église Saint-Germain des Prés",
"type":{
"landmark_type":"sightseeing"
},
"location":[
48.8539667,
2.334463
],
"osm_type":"way",
"osm_id":62287173,
"attractiveness":12,
"must_do":false,
"n_tags":21,
"time_to_reach":0
},
"25":{
"name":"Square de la Tour Saint-Jacques",
"type":{
"landmark_type":"nature"
},
"location":[
48.857913,
2.3487608
],
"osm_type":"way",
"osm_id":15895172,
"attractiveness":19,
"must_do":false,
"n_tags":12,
"time_to_reach":0
},
"26":{
"name":"Square Jean XXIII",
"type":{
"landmark_type":"nature"
},
"location":[
48.8523775,
2.3503406
],
"osm_type":"way",
"osm_id":20444455,
"attractiveness":23,
"must_do":false,
"n_tags":15,
"time_to_reach":0
},
"27":{
"name":"Square Félix Desruelles",
"type":{
"landmark_type":"nature"
},
"location":[
48.8536974,
2.3345069
],
"osm_type":"way",
"osm_id":62287123,
"attractiveness":12,
"must_do":false,
"n_tags":7,
"time_to_reach":0
},
"28":{
"name":"Jardin des Rosiers Joseph-Migneret",
"type":{
"landmark_type":"nature"
},
"location":[
48.8572946,
2.3602516
],
"osm_type":"way",
"osm_id":365878540,
"attractiveness":14,
"must_do":false,
"n_tags":8,
"time_to_reach":0
},
"29":{
"name":"Jardin du Père-Armand-David",
"type":{
"landmark_type":"nature"
},
"location":[
48.8478674,
2.3220972
],
"osm_type":"way",
"osm_id":588324415,
"attractiveness":10,
"must_do":false,
"n_tags":7,
"time_to_reach":0
}
}