From 193f837cc800c8c9ecc2edd12447e14f8973119b Mon Sep 17 00:00:00 2001 From: Martin Kedmenec Date: Tue, 31 Mar 2026 16:51:49 +0200 Subject: [PATCH 1/3] Update all packages --- backend/uv.lock | 68 +++---- frontend/package.json | 18 +- frontend/pnpm-lock.yaml | 406 ++++++++++++++++++---------------------- 3 files changed, 220 insertions(+), 272 deletions(-) diff --git a/backend/uv.lock b/backend/uv.lock index 069245b..9d3ace2 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -1302,18 +1302,18 @@ wheels = [ [[package]] name = "nodejs-wheel-binaries" -version = "24.14.0" +version = "24.14.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/05/c75c0940b1ebf82975d14f37176679b6f3229eae8b47b6a70d1e1dae0723/nodejs_wheel_binaries-24.14.0.tar.gz", hash = "sha256:c87b515e44b0e4a523017d8c59f26ccbd05b54fe593338582825d4b51fc91e1c", size = 8057, upload-time = "2026-02-27T02:57:30.931Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/87/e5755ad739daafce2e152ab609293d65e6c663b399e28a4bbcd0f4af1f45/nodejs_wheel_binaries-24.14.1.tar.gz", hash = "sha256:d00ae0c86d7e1bfa798e8f8ad282db751af157cdcaa1208a1b9a2cf2a85ac821", size = 8056, upload-time = "2026-03-31T14:07:27Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/8c/b057c2db3551a6fe04e93dd14e33d810ac8907891534ffcc7a051b253858/nodejs_wheel_binaries-24.14.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:59bb78b8eb08c3e32186da1ef913f1c806b5473d8bd0bb4492702092747b674a", size = 54798488, upload-time = "2026-02-27T02:56:56.831Z" }, - { url = "https://files.pythonhosted.org/packages/30/88/7e1b29c067b6625c97c81eb8b0ef37cf5ad5b62bb81e23f4bde804910ec9/nodejs_wheel_binaries-24.14.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:348fa061b57625de7250d608e2d9b7c4bc170544da7e328325343860eadd59e5", size = 54972803, upload-time = "2026-02-27T02:57:01.696Z" }, - { url = "https://files.pythonhosted.org/packages/1e/e0/a83f0ff12faca2a56366462e572e38ac6f5cb361877bb29e289138eb7f24/nodejs_wheel_binaries-24.14.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:222dbf516ccc877afcad4e4789a81b4ee93daaa9f0ad97c464417d9597f49449", size = 59340859, upload-time = "2026-02-27T02:57:06.125Z" }, - { url = "https://files.pythonhosted.org/packages/e2/9f/06fad4ae8a723ae7096b5311eba67ad8b4df5f359c0a68e366750b7fef78/nodejs_wheel_binaries-24.14.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:b35d6fcccfe4fb0a409392d237fbc67796bac0d357b996bc12d057a1531a238b", size = 59838751, upload-time = "2026-02-27T02:57:10.449Z" }, - { url = "https://files.pythonhosted.org/packages/8c/72/4916dadc7307c3e9bcfa43b4b6f88237932d502c66f89eb2d90fb07810db/nodejs_wheel_binaries-24.14.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:519507fb74f3f2b296ab1e9f00dcc211f36bbfb93c60229e72dcdee9dafd301a", size = 61340534, upload-time = "2026-02-27T02:57:15.309Z" }, - { url = "https://files.pythonhosted.org/packages/2e/df/a8ba881ee5d04b04e0d93abc8ce501ff7292813583e97f9789eb3fc0472a/nodejs_wheel_binaries-24.14.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:68c93c52ff06d704bcb5ed160b4ba04ab1b291d238aaf996b03a5396e0e9a7ed", size = 61922394, upload-time = "2026-02-27T02:57:20.24Z" }, - { url = "https://files.pythonhosted.org/packages/60/8c/b8c5f61201c72a0c7dc694b459941f89a6defda85deff258a9940a4e2efc/nodejs_wheel_binaries-24.14.0-py2.py3-none-win_amd64.whl", hash = "sha256:60b83c4e98b0c7d836ac9ccb67dcb36e343691cbe62cd325799ff9ed936286f3", size = 41218783, upload-time = "2026-02-27T02:57:24.175Z" }, - { url = "https://files.pythonhosted.org/packages/91/23/1f904bc9cbd8eece393e20840c08ba3ac03440090c3a4e95168fa6d2709f/nodejs_wheel_binaries-24.14.0-py2.py3-none-win_arm64.whl", hash = "sha256:78a9bd1d6b11baf1433f9fb84962ff8aa71c87d48b6434f98224bc49a2253a6e", size = 38926103, upload-time = "2026-02-27T02:57:27.458Z" }, + { url = "https://files.pythonhosted.org/packages/8b/b7/9765d9a5d3b95475829ef5965d4a4f6f4badb034ee4e18c2d5f8b9b65d6f/nodejs_wheel_binaries-24.14.1-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:d9e856ba0f2d3d2659869e6e0f4cae6874faeeeca7f879131a88451356373ac4", size = 54945603, upload-time = "2026-03-31T14:06:58.526Z" }, + { url = "https://files.pythonhosted.org/packages/6f/15/bc2fa51ee31ce597b2af1905081e5a5add07fe0cf619bfa531d7df2f1f1b/nodejs_wheel_binaries-24.14.1-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:634f57829ebfdfe95d096f32a50c5cdd3a6c72a94dcf2b92a8bef9868cccb13e", size = 55119951, upload-time = "2026-03-31T14:07:02.597Z" }, + { url = "https://files.pythonhosted.org/packages/a6/dd/92ff0831262af4bbb5473d4e7964fd27afb0901a2690a6ff7bc3d220d97f/nodejs_wheel_binaries-24.14.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:404b563467129e6a0ea7006a38b3d8af0ebfbc340b31a6a0af2c59ea3af90b7c", size = 59487620, upload-time = "2026-03-31T14:07:06.198Z" }, + { url = "https://files.pythonhosted.org/packages/45/36/bbbee3adf6afd00944e5a86ebd64987dea90bd347090155a4989dc3e8594/nodejs_wheel_binaries-24.14.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:7863c62f8a3946b727831f71375a9ae00205b3258478476034b49c3a1d57ac12", size = 59986044, upload-time = "2026-03-31T14:07:09.846Z" }, + { url = "https://files.pythonhosted.org/packages/05/16/119e4168bf7ed17ad7961d122701c75ac86135fa243a958b960a3f1b7055/nodejs_wheel_binaries-24.14.1-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a3f64daa1235fa6a83c778ded98d5fe4e74979ca54aa2ffb807f0805c57c3abe", size = 61489823, upload-time = "2026-03-31T14:07:13.378Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a6/d581996827b9d1133094dc347f1c4e3d2a70557973ce7a427a03337b1427/nodejs_wheel_binaries-24.14.1-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:810a48ce096925ead0690f7d143e48fb902ebfc9212097e8f6cb3ac6cbe8f314", size = 62069740, upload-time = "2026-03-31T14:07:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/396d1a48cbf3d5899461bda12fa97ae4010d7e4013e7f28230cb04af5818/nodejs_wheel_binaries-24.14.1-py2.py3-none-win_amd64.whl", hash = "sha256:7a087b6a727fb9242d1cc83c8b121711bd0e9686408d27de48b34b23dfb26ac5", size = 41400067, upload-time = "2026-03-31T14:07:20.513Z" }, + { url = "https://files.pythonhosted.org/packages/13/b7/adb21cf549934579e98934531e7f9b038d583fc9c2dd4b82ea01cc31bdd2/nodejs_wheel_binaries-24.14.1-py2.py3-none-win_arm64.whl", hash = "sha256:978fdfe76624c48111ab99ed0f99f9d4c1c682e420b0212ac9e1daee52f20283", size = 39096873, upload-time = "2026-03-31T14:07:23.977Z" }, ] [[package]] @@ -1394,31 +1394,31 @@ wheels = [ [[package]] name = "pandas" -version = "3.0.1" +version = "3.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "python-dateutil" }, { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/0c/b28ed414f080ee0ad153f848586d61d1878f91689950f037f976ce15f6c8/pandas-3.0.1.tar.gz", hash = "sha256:4186a699674af418f655dbd420ed87f50d56b4cd6603784279d9eef6627823c8", size = 4641901, upload-time = "2026-02-17T22:20:16.434Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/8b/4bb774a998b97e6c2fd62a9e6cfdaae133b636fd1c468f92afb4ae9a447a/pandas-3.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:99d0f92ed92d3083d140bf6b97774f9f13863924cf3f52a70711f4e7588f9d0a", size = 10322465, upload-time = "2026-02-17T22:19:36.803Z" }, - { url = "https://files.pythonhosted.org/packages/72/3a/5b39b51c64159f470f1ca3b1c2a87da290657ca022f7cd11442606f607d1/pandas-3.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3b66857e983208654294bb6477b8a63dee26b37bdd0eb34d010556e91261784f", size = 9910632, upload-time = "2026-02-17T22:19:39.001Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f7/b449ffb3f68c11da12fc06fbf6d2fa3a41c41e17d0284d23a79e1c13a7e4/pandas-3.0.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56cf59638bf24dc9bdf2154c81e248b3289f9a09a6d04e63608c159022352749", size = 10440535, upload-time = "2026-02-17T22:19:41.157Z" }, - { url = "https://files.pythonhosted.org/packages/55/77/6ea82043db22cb0f2bbfe7198da3544000ddaadb12d26be36e19b03a2dc5/pandas-3.0.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1a9f55e0f46951874b863d1f3906dcb57df2d9be5c5847ba4dfb55b2c815249", size = 10893940, upload-time = "2026-02-17T22:19:43.493Z" }, - { url = "https://files.pythonhosted.org/packages/03/30/f1b502a72468c89412c1b882a08f6eed8a4ee9dc033f35f65d0663df6081/pandas-3.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1849f0bba9c8a2fb0f691d492b834cc8dadf617e29015c66e989448d58d011ee", size = 11442711, upload-time = "2026-02-17T22:19:46.074Z" }, - { url = "https://files.pythonhosted.org/packages/0d/f0/ebb6ddd8fc049e98cabac5c2924d14d1dda26a20adb70d41ea2e428d3ec4/pandas-3.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3d288439e11b5325b02ae6e9cc83e6805a62c40c5a6220bea9beb899c073b1c", size = 11963918, upload-time = "2026-02-17T22:19:48.838Z" }, - { url = "https://files.pythonhosted.org/packages/09/f8/8ce132104074f977f907442790eaae24e27bce3b3b454e82faa3237ff098/pandas-3.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:93325b0fe372d192965f4cca88d97667f49557398bbf94abdda3bf1b591dbe66", size = 9862099, upload-time = "2026-02-17T22:19:51.081Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b7/6af9aac41ef2456b768ef0ae60acf8abcebb450a52043d030a65b4b7c9bd/pandas-3.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:97ca08674e3287c7148f4858b01136f8bdfe7202ad25ad04fec602dd1d29d132", size = 9185333, upload-time = "2026-02-17T22:19:53.266Z" }, - { url = "https://files.pythonhosted.org/packages/66/fc/848bb6710bc6061cb0c5badd65b92ff75c81302e0e31e496d00029fe4953/pandas-3.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:58eeb1b2e0fb322befcf2bbc9ba0af41e616abadb3d3414a6bc7167f6cbfce32", size = 10772664, upload-time = "2026-02-17T22:19:55.806Z" }, - { url = "https://files.pythonhosted.org/packages/69/5c/866a9bbd0f79263b4b0db6ec1a341be13a1473323f05c122388e0f15b21d/pandas-3.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cd9af1276b5ca9e298bd79a26bda32fa9cc87ed095b2a9a60978d2ca058eaf87", size = 10421286, upload-time = "2026-02-17T22:19:58.091Z" }, - { url = "https://files.pythonhosted.org/packages/51/a4/2058fb84fb1cfbfb2d4a6d485e1940bb4ad5716e539d779852494479c580/pandas-3.0.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f87a04984d6b63788327cd9f79dda62b7f9043909d2440ceccf709249ca988", size = 10342050, upload-time = "2026-02-17T22:20:01.376Z" }, - { url = "https://files.pythonhosted.org/packages/22/1b/674e89996cc4be74db3c4eb09240c4bb549865c9c3f5d9b086ff8fcfbf00/pandas-3.0.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85fe4c4df62e1e20f9db6ebfb88c844b092c22cd5324bdcf94bfa2fc1b391221", size = 10740055, upload-time = "2026-02-17T22:20:04.328Z" }, - { url = "https://files.pythonhosted.org/packages/d0/f8/e954b750764298c22fa4614376531fe63c521ef517e7059a51f062b87dca/pandas-3.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:331ca75a2f8672c365ae25c0b29e46f5ac0c6551fdace8eec4cd65e4fac271ff", size = 11357632, upload-time = "2026-02-17T22:20:06.647Z" }, - { url = "https://files.pythonhosted.org/packages/6d/02/c6e04b694ffd68568297abd03588b6d30295265176a5c01b7459d3bc35a3/pandas-3.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15860b1fdb1973fffade772fdb931ccf9b2f400a3f5665aef94a00445d7d8dd5", size = 11810974, upload-time = "2026-02-17T22:20:08.946Z" }, - { url = "https://files.pythonhosted.org/packages/89/41/d7dfb63d2407f12055215070c42fc6ac41b66e90a2946cdc5e759058398b/pandas-3.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:44f1364411d5670efa692b146c748f4ed013df91ee91e9bec5677fb1fd58b937", size = 10884622, upload-time = "2026-02-17T22:20:11.711Z" }, - { url = "https://files.pythonhosted.org/packages/68/b0/34937815889fa982613775e4b97fddd13250f11012d769949c5465af2150/pandas-3.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:108dd1790337a494aa80e38def654ca3f0968cf4f362c85f44c15e471667102d", size = 9452085, upload-time = "2026-02-17T22:20:14.331Z" }, + { url = "https://files.pythonhosted.org/packages/bb/40/c6ea527147c73b24fc15c891c3fcffe9c019793119c5742b8784a062c7db/pandas-3.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db0dbfd2a6cdf3770aa60464d50333d8f3d9165b2f2671bcc299b72de5a6677b", size = 10326084, upload-time = "2026-03-31T06:47:43.834Z" }, + { url = "https://files.pythonhosted.org/packages/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288", size = 9914146, upload-time = "2026-03-31T06:47:46.67Z" }, + { url = "https://files.pythonhosted.org/packages/8d/77/3a227ff3337aa376c60d288e1d61c5d097131d0ac71f954d90a8f369e422/pandas-3.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01f31a546acd5574ef77fe199bc90b55527c225c20ccda6601cf6b0fd5ed597c", size = 10444081, upload-time = "2026-03-31T06:47:49.681Z" }, + { url = "https://files.pythonhosted.org/packages/15/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535", size = 10897535, upload-time = "2026-03-31T06:47:53.033Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/98cc7a7624f7932e40f434299260e2917b090a579d75937cb8a57b9d2de3/pandas-3.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f48afd9bb13300ffb5a3316973324c787054ba6665cda0da3fbd67f451995db", size = 11446992, upload-time = "2026-03-31T06:47:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/19ff605cc3760e80602e6826ddef2824d8e7050ed80f2e11c4b079741dc3/pandas-3.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6c4d8458b97a35717b62469a4ea0e85abd5ed8687277f5ccfc67f8a5126f8c53", size = 11968257, upload-time = "2026-03-31T06:47:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf", size = 9865893, upload-time = "2026-03-31T06:48:02.038Z" }, + { url = "https://files.pythonhosted.org/packages/08/71/e5ec979dd2e8a093dacb8864598c0ff59a0cee0bbcdc0bfec16a51684d4f/pandas-3.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:63d141b56ef686f7f0d714cfb8de4e320475b86bf4b620aa0b7da89af8cbdbbb", size = 9188644, upload-time = "2026-03-31T06:48:05.045Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6c/7b45d85db19cae1eb524f2418ceaa9d85965dcf7b764ed151386b7c540f0/pandas-3.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:140f0cffb1fa2524e874dde5b477d9defe10780d8e9e220d259b2c0874c89d9d", size = 10776246, upload-time = "2026-03-31T06:48:07.789Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3e/7b00648b086c106e81766f25322b48aa8dfa95b55e621dbdf2fdd413a117/pandas-3.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae37e833ff4fed0ba352f6bdd8b73ba3ab3256a85e54edfd1ab51ae40cca0af8", size = 10424801, upload-time = "2026-03-31T06:48:10.897Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/558dd09a71b53b4008e7fc8a98ec6d447e9bfb63cdaeea10e5eb9b2dabe8/pandas-3.0.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d888a5c678a419a5bb41a2a93818e8ed9fd3172246555c0b37b7cc27027effd", size = 10345643, upload-time = "2026-03-31T06:48:13.7Z" }, + { url = "https://files.pythonhosted.org/packages/be/e3/921c93b4d9a280409451dc8d07b062b503bbec0531d2627e73a756e99a82/pandas-3.0.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b444dc64c079e84df91baa8bf613d58405645461cabca929d9178f2cd392398d", size = 10743641, upload-time = "2026-03-31T06:48:16.659Z" }, + { url = "https://files.pythonhosted.org/packages/56/ca/fd17286f24fa3b4d067965d8d5d7e14fe557dd4f979a0b068ac0deaf8228/pandas-3.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4544c7a54920de8eeacaa1466a6b7268ecfbc9bc64ab4dbb89c6bbe94d5e0660", size = 11361993, upload-time = "2026-03-31T06:48:19.475Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a5/2f6ed612056819de445a433ca1f2821ac3dab7f150d569a59e9cc105de1d/pandas-3.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:734be7551687c00fbd760dc0522ed974f82ad230d4a10f54bf51b80d44a08702", size = 11815274, upload-time = "2026-03-31T06:48:22.695Z" }, + { url = "https://files.pythonhosted.org/packages/00/2f/b622683e99ec3ce00b0854bac9e80868592c5b051733f2cf3a868e5fea26/pandas-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:57a07209bebcbcf768d2d13c9b78b852f9a15978dac41b9e6421a81ad4cdd276", size = 10888530, upload-time = "2026-03-31T06:48:25.806Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2b/f8434233fab2bd66a02ec014febe4e5adced20e2693e0e90a07d118ed30e/pandas-3.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482", size = 9455341, upload-time = "2026-03-31T06:48:28.418Z" }, ] [[package]] @@ -1924,7 +1924,7 @@ wheels = [ [[package]] name = "requests" -version = "2.33.0" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -1932,9 +1932,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]] @@ -2177,15 +2177,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.56.0" +version = "2.57.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/df/5008954f5466085966468612a7d1638487596ee6d2fd7fb51783a85351bf/sentry_sdk-2.56.0.tar.gz", hash = "sha256:fdab72030b69625665b2eeb9738bdde748ad254e8073085a0ce95382678e8168", size = 426820, upload-time = "2026-03-24T09:56:36.575Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/87/46c0406d8b5ddd026f73adaf5ab75ce144219c41a4830b52df4b9ab55f7f/sentry_sdk-2.57.0.tar.gz", hash = "sha256:4be8d1e71c32fb27f79c577a337ac8912137bba4bcbc64a4ec1da4d6d8dc5199", size = 435288, upload-time = "2026-03-31T09:39:29.264Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/1a/b3a3e9f6520493fed7997af4d2de7965d71549c62f994a8fd15f2ecd519e/sentry_sdk-2.56.0-py2.py3-none-any.whl", hash = "sha256:5afafb744ceb91d22f4cc650c6bd048ac6af5f7412dcc6c59305a2e36f4dbc02", size = 451568, upload-time = "2026-03-24T09:56:34.807Z" }, + { url = "https://files.pythonhosted.org/packages/c9/64/982e07b93219cb52e1cca5d272cb579e2f3eb001956c9e7a9a6d106c9473/sentry_sdk-2.57.0-py2.py3-none-any.whl", hash = "sha256:812c8bf5ff3d2f0e89c82f5ce80ab3a6423e102729c4706af7413fd1eb480585", size = 456489, upload-time = "2026-03-31T09:39:27.524Z" }, ] [[package]] diff --git a/frontend/package.json b/frontend/package.json index 659d332..d8bc1d0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,13 +14,13 @@ "tsc:check": "tsc -b --noEmit" }, "dependencies": { - "@mantine/core": "^8.3.18", - "@mantine/form": "^8.3.18", - "@mantine/hooks": "^8.3.18", + "@mantine/core": "^9.0.0", + "@mantine/form": "^9.0.0", + "@mantine/hooks": "^9.0.0", "@tabler/icons-react": "^3.41.1", - "@tanstack/query-async-storage-persister": "^5.95.2", - "@tanstack/react-query": "^5.95.2", - "@tanstack/react-query-persist-client": "^5.95.2", + "@tanstack/query-async-storage-persister": "^5.96.0", + "@tanstack/react-query": "^5.96.0", + "@tanstack/react-query-persist-client": "^5.96.0", "@turf/boolean-point-in-polygon": "^7.3.4", "@turf/helpers": "^7.3.4", "axios": "^1.14.0", @@ -35,8 +35,8 @@ "@eslint/js": "^9.39.4", "@hey-api/openapi-ts": "0.94.5", "@rolldown/plugin-babel": "^0.2.2", - "@tanstack/eslint-plugin-query": "^5.95.2", - "@tanstack/react-query-devtools": "^5.95.2", + "@tanstack/eslint-plugin-query": "^5.96.0", + "@tanstack/react-query-devtools": "^5.96.0", "@types/babel__core": "^7.20.5", "@types/geojson": "^7946.0.16", "@types/leaflet": "^1.9.21", @@ -57,7 +57,7 @@ "postcss-simple-vars": "^7.0.1", "prettier": "3.8.1", "typescript": "~5.9.3", - "typescript-eslint": "^8.57.2", + "typescript-eslint": "^8.58.0", "vite": "^8.0.3" }, "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 1ffdf51..af0c750 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -9,26 +9,26 @@ importers: .: dependencies: '@mantine/core': - specifier: ^8.3.18 - version: 8.3.18(@mantine/hooks@8.3.18(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^9.0.0 + version: 9.0.0(@mantine/hooks@9.0.0(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@mantine/form': - specifier: ^8.3.18 - version: 8.3.18(react@19.2.4) + specifier: ^9.0.0 + version: 9.0.0(react@19.2.4) '@mantine/hooks': - specifier: ^8.3.18 - version: 8.3.18(react@19.2.4) + specifier: ^9.0.0 + version: 9.0.0(react@19.2.4) '@tabler/icons-react': specifier: ^3.41.1 version: 3.41.1(react@19.2.4) '@tanstack/query-async-storage-persister': - specifier: ^5.95.2 - version: 5.95.2 + specifier: ^5.96.0 + version: 5.96.0 '@tanstack/react-query': - specifier: ^5.95.2 - version: 5.95.2(react@19.2.4) + specifier: ^5.96.0 + version: 5.96.0(react@19.2.4) '@tanstack/react-query-persist-client': - specifier: ^5.95.2 - version: 5.95.2(@tanstack/react-query@5.95.2(react@19.2.4))(react@19.2.4) + specifier: ^5.96.0 + version: 5.96.0(@tanstack/react-query@5.96.0(react@19.2.4))(react@19.2.4) '@turf/boolean-point-in-polygon': specifier: ^7.3.4 version: 7.3.4 @@ -67,11 +67,11 @@ importers: specifier: ^0.2.2 version: 0.2.2(@babel/core@7.29.0)(@babel/runtime@7.29.2)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1))(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.8))) '@tanstack/eslint-plugin-query': - specifier: ^5.95.2 - version: 5.95.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + specifier: ^5.96.0 + version: 5.96.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) '@tanstack/react-query-devtools': - specifier: ^5.95.2 - version: 5.95.2(@tanstack/react-query@5.95.2(react@19.2.4))(react@19.2.4) + specifier: ^5.96.0 + version: 5.96.0(@tanstack/react-query@5.96.0(react@19.2.4))(react@19.2.4) '@types/babel__core': specifier: ^7.20.5 version: 7.20.5 @@ -101,7 +101,7 @@ importers: version: 9.39.4(jiti@2.6.1) eslint-config-mantine: specifier: ^4.0.3 - version: 4.0.3(@eslint/js@9.39.4)(eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.6.1)))(eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(typescript-eslint@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)) + version: 4.0.3(@eslint/js@9.39.4)(eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.6.1)))(eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(typescript-eslint@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)) eslint-config-prettier: specifier: ^10.1.8 version: 10.1.8(eslint@9.39.4(jiti@2.6.1)) @@ -133,8 +133,8 @@ importers: specifier: ~5.9.3 version: 5.9.3 typescript-eslint: - specifier: ^8.57.2 - version: 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + specifier: ^8.58.0 + version: 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^8.0.3 version: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.8)) @@ -496,22 +496,22 @@ packages: '@jsdevtools/ono@7.1.3': resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} - '@mantine/core@8.3.18': - resolution: {integrity: sha512-9tph1lTVogKPjTx02eUxDUOdXacPzK62UuSqb4TdGliI54/Xgxftq0Dfqu6XuhCxn9J5MDJaNiLDvL/1KRkYqA==} + '@mantine/core@9.0.0': + resolution: {integrity: sha512-GUUXlf+4uDpRu5iZ1PFy1NmgG7ttI+1I6VTw3v/b7N1jwOUhARwL1moGjPMjo3Eie3tHdVOCiVMYAvhlQ1GI5Q==} peerDependencies: - '@mantine/hooks': 8.3.18 - react: ^18.x || ^19.x - react-dom: ^18.x || ^19.x + '@mantine/hooks': 9.0.0 + react: ^19.2.0 + react-dom: ^19.2.0 - '@mantine/form@8.3.18': - resolution: {integrity: sha512-r5OGLJWTkmIruFjRZRZy9oA7maNYlyt50jB4Pmd2X5360WOmJLd4KH8MFhHZQC7vN+z8/rmBl3t3XGAR2I8xig==} + '@mantine/form@9.0.0': + resolution: {integrity: sha512-13YUrYAyzvwwPQL5X+yMb0lbtiv2pfyrU/hvVWAitm5X7twuMYrHBlpci+umdKZnsrY6Q/6M5HHsToVOrZ6rvA==} peerDependencies: - react: ^18.x || ^19.x + react: ^19.2.0 - '@mantine/hooks@8.3.18': - resolution: {integrity: sha512-QoWr9+S8gg5050TQ06aTSxtlpGjYOpIllRbjYYXlRvZeTsUqiTbVfvQROLexu4rEaK+yy9Wwriwl9PMRgbLqPw==} + '@mantine/hooks@9.0.0': + resolution: {integrity: sha512-TKcz+k0JH/jtblBfwOw/vXBX2EpJO66psJ5ZVmdDhwc6vbHDsvY6oYN8ynt9TfRn/eHZXsEmLPNj+wuGtWy4BA==} peerDependencies: - react: ^18.x || ^19.x + react: ^19.2.0 '@microsoft/tsdoc-config@0.18.1': resolution: {integrity: sha512-9brPoVdfN9k9g0dcWkFeA7IH9bbcttzDJlXvkf8b2OBzd5MueR1V2wkKBL0abn0otvmkHJC6aapBOTJDDeMCZg==} @@ -678,8 +678,8 @@ packages: '@tabler/icons@3.41.1': resolution: {integrity: sha512-OaRnVbRmH2nHtFeg+RmMJ/7m2oBIF9XCJAUD5gQnMrpK9f05ydj8MZrAf3NZQqOXyxGN1UBL0D5IKLLEUfr74Q==} - '@tanstack/eslint-plugin-query@5.95.2': - resolution: {integrity: sha512-EYUFRaqjBep4EHMPpZR12sXP7Kr5qv9iDIlq93NfbhHwhITaW6Txu3ROO6dLFz5r84T8p+oZXBG77pa2Wuok7A==} + '@tanstack/eslint-plugin-query@5.96.0': + resolution: {integrity: sha512-iPxSM1lNBzz63scYaudGeJk4yqb51MpvnX2GUmBjEvoLLwcaCTZg+OQmjmaoe5o3TApQZK8INYnmYhN4p4uxuQ==} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ^5.4.0 @@ -687,32 +687,32 @@ packages: typescript: optional: true - '@tanstack/query-async-storage-persister@5.95.2': - resolution: {integrity: sha512-ZhPIHH8J833OVZhEWwwdOk0uhY94d9Wgdnq97JoQx4Ui4xx4Dh6e7WPUrjlUWo88Yqi4Ij+T1o/VR7Vlbnkbjw==} + '@tanstack/query-async-storage-persister@5.96.0': + resolution: {integrity: sha512-Xlzt5UFyAkSDkZ7DwlgpPU6AHMRwn6RKp+fbyuburrPhy94/oHbX+X4jNoq+fdPzSYC8U1Y6yOfpkvRAov7ZaQ==} - '@tanstack/query-core@5.95.2': - resolution: {integrity: sha512-o4T8vZHZET4Bib3jZ/tCW9/7080urD4c+0/AUaYVpIqOsr7y0reBc1oX3ttNaSW5mYyvZHctiQ/UOP2PfdmFEQ==} + '@tanstack/query-core@5.96.0': + resolution: {integrity: sha512-sfO3uQeol1BU7cRP6NYY7nAiX3GiNY20lI/dtSbKLwcIkYw/X+w/tEsQAkc544AfIhBX/IvH/QYtPHrPhyAKGw==} - '@tanstack/query-devtools@5.95.2': - resolution: {integrity: sha512-QfaoqBn9uAZ+ICkA8brd1EHj+qBF6glCFgt94U8XP5BT6ppSsDBI8IJ00BU+cAGjQzp6wcKJL2EmRYvxy0TWIg==} + '@tanstack/query-devtools@5.96.0': + resolution: {integrity: sha512-MEdO1M/9ItB62OtTqVo8AIj/G6vJemA642N56bw8aIqpXKIj5VG/3xWgh2piw76NmoCIlapxjjWp1MMLmrvKJw==} - '@tanstack/query-persist-client-core@5.95.2': - resolution: {integrity: sha512-Opfj34WZ594YXpEcZEs8WBiyPGrjrKlGILfk/Ss283uwWQ36C5nX3tRY/bBiXmM82KWauUuNvahwGwiyco/8cQ==} + '@tanstack/query-persist-client-core@5.96.0': + resolution: {integrity: sha512-iSqqhWtQ7sZqucmOIbC+eU+npaP36ZARtyjEfaDL/9A8f8P70NHe3RJ3N7fkxbkc36pQd+duANo5TTnq8yCZSg==} - '@tanstack/react-query-devtools@5.95.2': - resolution: {integrity: sha512-AFQFmbznVkbtfpx8VJ2DylW17wWagQel/qLstVLkYmNRo2CmJt3SNej5hvl6EnEeljJIdC3BTB+W7HZtpsH+3g==} + '@tanstack/react-query-devtools@5.96.0': + resolution: {integrity: sha512-P0WFX0s3iYii4oZTSCK9T3/PBQ9uY/SVTzcZFbyzCo5ujeIAsqos3HjBWoF/lhJXWVe8UXkjAmgXr3TUD11q2A==} peerDependencies: - '@tanstack/react-query': ^5.95.2 + '@tanstack/react-query': ^5.96.0 react: ^18 || ^19 - '@tanstack/react-query-persist-client@5.95.2': - resolution: {integrity: sha512-i3fvzD8gaLgQyFvRc/+iSUr60aL31tMN+5QM11zdPRg0K9CirIQjHD7WgXFBnD29KJDvcjcv7OrIBaPwZ+H9xw==} + '@tanstack/react-query-persist-client@5.96.0': + resolution: {integrity: sha512-lXOIHU2i+GAG7Gm3OEMmw3xmD51H/8i99tIFaBcGW4mF0Wq91q3Q78xf5q7Cu0NI8WRcbxi7Dn6h1Fs9zMNw0A==} peerDependencies: - '@tanstack/react-query': ^5.95.2 + '@tanstack/react-query': ^5.96.0 react: ^18 || ^19 - '@tanstack/react-query@5.95.2': - resolution: {integrity: sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA==} + '@tanstack/react-query@5.96.0': + resolution: {integrity: sha512-6qbjdm1K5kizVKv9TNqhIN3doq2anRhdF2XaFMFSn4m8L22S69RV+FilvlyVT4RoJyMxtPU5rs4RpdFa/PEC7A==} peerDependencies: react: ^18 || ^19 @@ -793,20 +793,20 @@ packages: '@types/use-sync-external-store@0.0.6': resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} - '@typescript-eslint/eslint-plugin@8.57.2': - resolution: {integrity: sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==} + '@typescript-eslint/eslint-plugin@8.58.0': + resolution: {integrity: sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.57.2 + '@typescript-eslint/parser': ^8.58.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.57.2': - resolution: {integrity: sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==} + '@typescript-eslint/parser@8.58.0': + resolution: {integrity: sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' '@typescript-eslint/project-service@8.56.1': resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} @@ -814,18 +814,18 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.57.2': - resolution: {integrity: sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==} + '@typescript-eslint/project-service@8.58.0': + resolution: {integrity: sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' '@typescript-eslint/scope-manager@8.56.1': resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.57.2': - resolution: {integrity: sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==} + '@typescript-eslint/scope-manager@8.58.0': + resolution: {integrity: sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/tsconfig-utils@8.56.1': @@ -834,25 +834,25 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/tsconfig-utils@8.57.2': - resolution: {integrity: sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==} + '@typescript-eslint/tsconfig-utils@8.58.0': + resolution: {integrity: sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.57.2': - resolution: {integrity: sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==} + '@typescript-eslint/type-utils@8.58.0': + resolution: {integrity: sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' '@typescript-eslint/types@8.56.1': resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.57.2': - resolution: {integrity: sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==} + '@typescript-eslint/types@8.58.0': + resolution: {integrity: sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/typescript-estree@8.56.1': @@ -861,11 +861,11 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/typescript-estree@8.57.2': - resolution: {integrity: sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==} + '@typescript-eslint/typescript-estree@8.58.0': + resolution: {integrity: sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' '@typescript-eslint/utils@8.56.1': resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} @@ -874,19 +874,19 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.57.2': - resolution: {integrity: sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==} + '@typescript-eslint/utils@8.58.0': + resolution: {integrity: sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' '@typescript-eslint/visitor-keys@8.56.1': resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.57.2': - resolution: {integrity: sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==} + '@typescript-eslint/visitor-keys@8.58.0': + resolution: {integrity: sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@vitejs/plugin-react@6.0.1': @@ -975,8 +975,8 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axe-core@4.11.1: - resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} + axe-core@4.11.2: + resolution: {integrity: sha512-byD6KPdvo72y/wj2T/4zGEvvlis+PsZsn/yPS3pEO+sFpcrqRpX/TJCxvVaEsNeMrfQbCr7w163YqoD9IYwHXw==} engines: {node: '>=4'} axios@1.14.0: @@ -1008,8 +1008,8 @@ packages: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} - browserslist@4.28.1: - resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -1233,8 +1233,8 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - electron-to-chromium@1.5.328: - resolution: {integrity: sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==} + electron-to-chromium@1.5.329: + resolution: {integrity: sha512-/4t+AS1l4S3ZC0Ja7PHFIWeBIxGA3QGqV8/yKsP36v7NcyUCl+bIcmw6s5zVuMIECWwBrAK/6QLzTmbJChBboQ==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} @@ -1847,8 +1847,8 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - minimatch@10.2.4: - resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} minimatch@3.1.5: @@ -2096,12 +2096,6 @@ packages: '@types/react': optional: true - react-textarea-autosize@8.5.9: - resolution: {integrity: sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==} - engines: {node: '>=10'} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react@19.2.4: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} @@ -2287,6 +2281,10 @@ packages: tabbable@6.4.0: resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -2311,9 +2309,9 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-fest@4.41.0: - resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} - engines: {node: '>=16'} + type-fest@5.5.0: + resolution: {integrity: sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==} + engines: {node: '>=20'} typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} @@ -2331,12 +2329,12 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript-eslint@8.57.2: - resolution: {integrity: sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==} + typescript-eslint@8.58.0: + resolution: {integrity: sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} @@ -2369,33 +2367,6 @@ packages: '@types/react': optional: true - use-composed-ref@1.4.0: - resolution: {integrity: sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - - use-isomorphic-layout-effect@1.2.1: - resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - - use-latest@1.3.0: - resolution: {integrity: sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - use-sidecar@1.1.3: resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} engines: {node: '>=10'} @@ -2547,7 +2518,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.0 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.1 + browserslist: 4.28.2 lru-cache: 5.1.1 semver: 6.3.1 @@ -2584,7 +2555,8 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@babel/runtime@7.29.2': {} + '@babel/runtime@7.29.2': + optional: true '@babel/template@7.28.6': dependencies: @@ -2855,27 +2827,27 @@ snapshots: '@jsdevtools/ono@7.1.3': {} - '@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@mantine/core@9.0.0(@mantine/hooks@9.0.0(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@floating-ui/react': 0.27.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@mantine/hooks': 8.3.18(react@19.2.4) + '@mantine/hooks': 9.0.0(react@19.2.4) clsx: 2.1.1 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) react-number-format: 5.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) - react-textarea-autosize: 8.5.9(@types/react@19.2.14)(react@19.2.4) - type-fest: 4.41.0 + type-fest: 5.5.0 transitivePeerDependencies: - '@types/react' - '@mantine/form@8.3.18(react@19.2.4)': + '@mantine/form@9.0.0(react@19.2.4)': dependencies: + '@standard-schema/spec': 1.1.0 fast-deep-equal: 3.1.3 klona: 2.0.6 react: 19.2.4 - '@mantine/hooks@8.3.18(react@19.2.4)': + '@mantine/hooks@9.0.0(react@19.2.4)': dependencies: react: 19.2.4 @@ -2989,43 +2961,43 @@ snapshots: '@tabler/icons@3.41.1': {} - '@tanstack/eslint-plugin-query@5.95.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + '@tanstack/eslint-plugin-query@5.96.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/utils': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.4(jiti@2.6.1) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@tanstack/query-async-storage-persister@5.95.2': + '@tanstack/query-async-storage-persister@5.96.0': dependencies: - '@tanstack/query-core': 5.95.2 - '@tanstack/query-persist-client-core': 5.95.2 + '@tanstack/query-core': 5.96.0 + '@tanstack/query-persist-client-core': 5.96.0 - '@tanstack/query-core@5.95.2': {} + '@tanstack/query-core@5.96.0': {} - '@tanstack/query-devtools@5.95.2': {} + '@tanstack/query-devtools@5.96.0': {} - '@tanstack/query-persist-client-core@5.95.2': + '@tanstack/query-persist-client-core@5.96.0': dependencies: - '@tanstack/query-core': 5.95.2 + '@tanstack/query-core': 5.96.0 - '@tanstack/react-query-devtools@5.95.2(@tanstack/react-query@5.95.2(react@19.2.4))(react@19.2.4)': + '@tanstack/react-query-devtools@5.96.0(@tanstack/react-query@5.96.0(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/query-devtools': 5.95.2 - '@tanstack/react-query': 5.95.2(react@19.2.4) + '@tanstack/query-devtools': 5.96.0 + '@tanstack/react-query': 5.96.0(react@19.2.4) react: 19.2.4 - '@tanstack/react-query-persist-client@5.95.2(@tanstack/react-query@5.95.2(react@19.2.4))(react@19.2.4)': + '@tanstack/react-query-persist-client@5.96.0(@tanstack/react-query@5.96.0(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/query-persist-client-core': 5.95.2 - '@tanstack/react-query': 5.95.2(react@19.2.4) + '@tanstack/query-persist-client-core': 5.96.0 + '@tanstack/react-query': 5.96.0(react@19.2.4) react: 19.2.4 - '@tanstack/react-query@5.95.2(react@19.2.4)': + '@tanstack/react-query@5.96.0(react@19.2.4)': dependencies: - '@tanstack/query-core': 5.95.2 + '@tanstack/query-core': 5.96.0 react: 19.2.4 '@turf/boolean-point-in-polygon@7.3.4': @@ -3121,14 +3093,14 @@ snapshots: '@types/use-sync-external-store@0.0.6': {} - '@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/type-utils': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.2 + '@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/type-utils': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.58.0 eslint: 9.39.4(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 @@ -3137,12 +3109,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.2 + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.58.0 debug: 4.4.3 eslint: 9.39.4(jiti@2.6.1) typescript: 5.9.3 @@ -3158,10 +3130,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.57.2(typescript@5.9.3)': + '@typescript-eslint/project-service@8.58.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3) - '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@5.9.3) + '@typescript-eslint/types': 8.58.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: @@ -3172,24 +3144,24 @@ snapshots: '@typescript-eslint/types': 8.56.1 '@typescript-eslint/visitor-keys': 8.56.1 - '@typescript-eslint/scope-manager@8.57.2': + '@typescript-eslint/scope-manager@8.58.0': dependencies: - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/visitor-keys': 8.57.2 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/visitor-keys': 8.58.0 '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.57.2(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.58.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 eslint: 9.39.4(jiti@2.6.1) ts-api-utils: 2.5.0(typescript@5.9.3) @@ -3199,7 +3171,7 @@ snapshots: '@typescript-eslint/types@8.56.1': {} - '@typescript-eslint/types@8.57.2': {} + '@typescript-eslint/types@8.58.0': {} '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': dependencies: @@ -3208,7 +3180,7 @@ snapshots: '@typescript-eslint/types': 8.56.1 '@typescript-eslint/visitor-keys': 8.56.1 debug: 4.4.3 - minimatch: 10.2.4 + minimatch: 10.2.5 semver: 7.7.4 tinyglobby: 0.2.15 ts-api-utils: 2.5.0(typescript@5.9.3) @@ -3216,14 +3188,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.58.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.57.2(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3) - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/visitor-keys': 8.57.2 + '@typescript-eslint/project-service': 8.58.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@5.9.3) + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/visitor-keys': 8.58.0 debug: 4.4.3 - minimatch: 10.2.4 + minimatch: 10.2.5 semver: 7.7.4 tinyglobby: 0.2.15 ts-api-utils: 2.5.0(typescript@5.9.3) @@ -3242,12 +3214,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) eslint: 9.39.4(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -3258,9 +3230,9 @@ snapshots: '@typescript-eslint/types': 8.56.1 eslint-visitor-keys: 5.0.1 - '@typescript-eslint/visitor-keys@8.57.2': + '@typescript-eslint/visitor-keys@8.58.0': dependencies: - '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/types': 8.58.0 eslint-visitor-keys: 5.0.1 '@vitejs/plugin-react@6.0.1(@rolldown/plugin-babel@0.2.2(@babel/core@7.29.0)(@babel/runtime@7.29.2)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1))(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.8))))(babel-plugin-react-compiler@1.0.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.8)))': @@ -3368,7 +3340,7 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - axe-core@4.11.1: {} + axe-core@4.11.2: {} axios@1.14.0: dependencies: @@ -3399,13 +3371,13 @@ snapshots: dependencies: balanced-match: 4.0.4 - browserslist@4.28.1: + browserslist@4.28.2: dependencies: baseline-browser-mapping: 2.10.12 caniuse-lite: 1.0.30001782 - electron-to-chromium: 1.5.328 + electron-to-chromium: 1.5.329 node-releases: 2.0.36 - update-browserslist-db: 1.2.3(browserslist@4.28.1) + update-browserslist-db: 1.2.3(browserslist@4.28.2) bundle-name@4.1.0: dependencies: @@ -3607,7 +3579,7 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - electron-to-chromium@1.5.328: {} + electron-to-chromium@1.5.329: {} emoji-regex@9.2.2: {} @@ -3749,13 +3721,13 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-mantine@4.0.3(@eslint/js@9.39.4)(eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.6.1)))(eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(typescript-eslint@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)): + eslint-config-mantine@4.0.3(@eslint/js@9.39.4)(eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.6.1)))(eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(typescript-eslint@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)): dependencies: '@eslint/js': 9.39.4 eslint: 9.39.4(jiti@2.6.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1)) - typescript-eslint: 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + typescript-eslint: 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.6.1)): dependencies: @@ -3767,7 +3739,7 @@ snapshots: array-includes: 3.1.9 array.prototype.flatmap: 1.3.3 ast-types-flow: 0.0.8 - axe-core: 4.11.1 + axe-core: 4.11.2 axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 @@ -4318,7 +4290,7 @@ snapshots: dependencies: mime-db: 1.52.0 - minimatch@10.2.4: + minimatch@10.2.5: dependencies: brace-expansion: 5.0.5 @@ -4564,15 +4536,6 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - react-textarea-autosize@8.5.9(@types/react@19.2.14)(react@19.2.4): - dependencies: - '@babel/runtime': 7.29.2 - react: 19.2.4 - use-composed-ref: 1.4.0(@types/react@19.2.14)(react@19.2.4) - use-latest: 1.3.0(@types/react@19.2.14)(react@19.2.4) - transitivePeerDependencies: - - '@types/react' - react@19.2.4: {} readdirp@5.0.0: {} @@ -4828,6 +4791,8 @@ snapshots: tabbable@6.4.0: {} + tagged-tag@1.0.0: {} + tiny-invariant@1.3.3: {} tinyexec@1.0.4: {} @@ -4847,7 +4812,9 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-fest@4.41.0: {} + type-fest@5.5.0: + dependencies: + tagged-tag: 1.0.0 typed-array-buffer@1.0.3: dependencies: @@ -4882,12 +4849,12 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.4(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -4904,9 +4871,9 @@ snapshots: undici-types@7.16.0: {} - update-browserslist-db@1.2.3(browserslist@4.28.1): + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: - browserslist: 4.28.1 + browserslist: 4.28.2 escalade: 3.2.0 picocolors: 1.1.1 @@ -4921,25 +4888,6 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - use-composed-ref@1.4.0(@types/react@19.2.14)(react@19.2.4): - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - use-isomorphic-layout-effect@1.2.1(@types/react@19.2.14)(react@19.2.4): - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - use-latest@1.3.0(@types/react@19.2.14)(react@19.2.4): - dependencies: - react: 19.2.4 - use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.14)(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.4): dependencies: detect-node-es: 1.1.0 From d508da59e30203da24b78c285e7e041961bc287c Mon Sep 17 00:00:00 2001 From: Martin Kedmenec Date: Thu, 2 Apr 2026 15:01:21 +0200 Subject: [PATCH 2/3] Rename types and variables globally --- backend/app/costs.py | 108 ++-- backend/app/geocoding.py | 22 +- backend/app/graph_state.py | 62 +- backend/app/layer_service.py | 82 +-- backend/app/main.py | 156 ++--- backend/app/models.py | 82 +-- backend/app/overlays.py | 105 ++-- backend/app/route_planner.py | 406 ++++++------- backend/app/typing_aliases.py | 2 +- backend/app/value_parsing.py | 2 +- .../data/overlays/{uphill.json => hills.json} | 0 backend/pyproject.toml | 2 +- backend/test.http | 65 +- backend/tests/conftest.py | 2 +- backend/tests/test_costs.py | 42 +- backend/tests/test_geocoding_and_main.py | 88 +-- backend/tests/test_graph_state.py | 50 +- backend/tests/test_layer_service.py | 44 +- backend/tests/test_overlays.py | 24 +- backend/tests/test_route_planner.py | 129 +--- .../tests/test_value_parsing_and_models.py | 18 +- backend/uv.lock | 176 +++--- frontend/package.json | 12 +- frontend/pnpm-lock.yaml | 148 ++--- frontend/src/App.tsx | 564 ++++++++++-------- frontend/src/Map.tsx | 74 +-- frontend/src/MarkerPickController.tsx | 10 +- frontend/src/OriginDestinationMarkers.tsx | 2 +- ...{AttributeOverlay.tsx => OverlayLayer.tsx} | 52 +- ...eStatsPanel.tsx => RouteAnalysisPanel.tsx} | 125 ++-- frontend/src/RouteLayer.tsx | 4 +- frontend/src/RoutePanel.tsx | 332 ++++++----- ...roller.tsx => RouteViewportController.tsx} | 24 +- frontend/src/SelectedSegment.tsx | 43 +- .../src/{StatusBar.tsx => StatusNotice.tsx} | 4 +- frontend/src/constants.ts | 10 +- frontend/src/route-metrics.ts | 82 ++- frontend/src/types/global.ts | 8 +- frontend/src/utils.ts | 6 +- 39 files changed, 1537 insertions(+), 1630 deletions(-) rename backend/data/overlays/{uphill.json => hills.json} (100%) rename frontend/src/{AttributeOverlay.tsx => OverlayLayer.tsx} (55%) rename frontend/src/{RouteStatsPanel.tsx => RouteAnalysisPanel.tsx} (71%) rename frontend/src/{RouteBoundsController.tsx => RouteViewportController.tsx} (59%) rename frontend/src/{StatusBar.tsx => StatusNotice.tsx} (81%) diff --git a/backend/app/costs.py b/backend/app/costs.py index 3790f03..59de8af 100644 --- a/backend/app/costs.py +++ b/backend/app/costs.py @@ -3,16 +3,16 @@ from collections.abc import Callable, Mapping from typing import TYPE_CHECKING, cast -from app.models import NormalizedRouteObjectiveWeights, RouteObjectiveWeights -from app.value_parsing import coerce_float +from app.models import NormalizedRoutePreferenceWeights, RoutePreferenceWeights +from app.value_parsing import parse_float_or_default if TYPE_CHECKING: - from app.typing_aliases import EdgeAttributes, MultiDiGraphAny + from app.typing_aliases import EdgeAttributeMap, MultiDiGraphAny FALLBACK_EDGE_COST = 1e18 -def _coerce_edge_attributes(candidate: object) -> EdgeAttributes | None: +def _coerce_edge_attribute_mapping(candidate: object) -> EdgeAttributeMap | None: """Convert mapping-like edge payload to a string-keyed dict.""" if not isinstance(candidate, Mapping): return None @@ -27,15 +27,17 @@ def _coerce_edge_attributes(candidate: object) -> EdgeAttributes | None: } -def extract_parallel_edge_attributes(candidate: object) -> list[EdgeAttributes]: +def _extract_parallel_edge_attribute_mappings( + candidate: object, +) -> list[EdgeAttributeMap]: """Extract parallel edge attribute dictionaries from a NetworkX payload.""" - parallel_edges: list[EdgeAttributes] = [] + parallel_edges: list[EdgeAttributeMap] = [] if not isinstance(candidate, Mapping): return parallel_edges for edge_candidate in candidate.values(): - edge_attributes = _coerce_edge_attributes(edge_candidate) + edge_attributes = _coerce_edge_attribute_mapping(edge_candidate) if edge_attributes is not None: parallel_edges.append(edge_attributes) @@ -43,61 +45,61 @@ def extract_parallel_edge_attributes(candidate: object) -> list[EdgeAttributes]: return parallel_edges -def normalize_route_objective_weights( - route_objective_weights: RouteObjectiveWeights, -) -> NormalizedRouteObjectiveWeights: +def normalize_route_preference_weights( + route_preference_weights: RoutePreferenceWeights, +) -> NormalizedRoutePreferenceWeights: """Normalize objective weights from 0-100 percentages to 0.0-1.0.""" - return NormalizedRouteObjectiveWeights( - scenic=route_objective_weights.scenic / 100.0, - avoid_snow=route_objective_weights.avoid_snow / 100.0, - avoid_uphill=route_objective_weights.avoid_uphill / 100.0, + return NormalizedRoutePreferenceWeights( + scenic_weight=route_preference_weights.scenic_weight / 100.0, + snow_free_weight=route_preference_weights.snow_free_weight / 100.0, + flat_weight=route_preference_weights.flat_weight / 100.0, ) -def compute_edge_objective_components( - edge_attributes: EdgeAttributes, +def compute_edge_cost_components( + edge_attributes: EdgeAttributeMap, ) -> tuple[float, float, float, float]: """Compute distance and objective-aligned edge costs.""" - distance_meters = coerce_float(edge_attributes.get("length"), default=0.0) + distance_meters = parse_float_or_default(edge_attributes.get("length"), default=0.0) - snow_exposure = coerce_float(edge_attributes.get("snow"), default=0.0) - uphill_exposure = coerce_float(edge_attributes.get("uphill"), default=0.0) - scenic_score = coerce_float(edge_attributes.get("scenic"), default=0.0) + snow_intensity = parse_float_or_default(edge_attributes.get("snow"), default=0.0) + hill_intensity = parse_float_or_default(edge_attributes.get("hills"), default=0.0) + scenic_value = parse_float_or_default(edge_attributes.get("scenic"), default=0.0) - snow_penalty_cost = distance_meters * snow_exposure - uphill_penalty_cost = distance_meters * uphill_exposure - scenic_penalty_cost = distance_meters * (1.0 - scenic_score) + snow_penalty = distance_meters * snow_intensity + hill_penalty = distance_meters * hill_intensity + scenic_penalty = distance_meters * (1.0 - scenic_value) return ( distance_meters, - snow_penalty_cost, - uphill_penalty_cost, - scenic_penalty_cost, + snow_penalty, + hill_penalty, + scenic_penalty, ) -def compute_scalar_edge_cost( - edge_attributes: EdgeAttributes, - normalized_weights: NormalizedRouteObjectiveWeights, +def _compute_weighted_edge_cost( + edge_attributes: EdgeAttributeMap, + normalized_weights: NormalizedRoutePreferenceWeights, ) -> float: """Compute a single scalar cost for an edge.""" ( distance_meters, - snow_penalty_cost, - uphill_penalty_cost, - scenic_penalty_cost, - ) = compute_edge_objective_components(edge_attributes) + snow_penalty, + hill_penalty, + scenic_penalty, + ) = compute_edge_cost_components(edge_attributes) return ( distance_meters - + normalized_weights.avoid_snow * snow_penalty_cost - + normalized_weights.avoid_uphill * uphill_penalty_cost - + normalized_weights.scenic * scenic_penalty_cost + + normalized_weights.snow_free_weight * snow_penalty + + normalized_weights.flat_weight * hill_penalty + + normalized_weights.scenic_weight * scenic_penalty ) -def build_networkx_weight_function( - normalized_weights: NormalizedRouteObjectiveWeights, +def build_weighted_edge_cost_function( + normalized_weights: NormalizedRoutePreferenceWeights, ) -> Callable[[int, int, object], float]: """Create a weight function compatible with NetworkX MultiDiGraph.""" @@ -106,12 +108,14 @@ def edge_weight( _target_node_id: int, networkx_edge_payload: object, ) -> float: - direct_edge_attributes = _coerce_edge_attributes(networkx_edge_payload) + direct_edge_attributes = _coerce_edge_attribute_mapping(networkx_edge_payload) if direct_edge_attributes is not None and "length" in direct_edge_attributes: - return compute_scalar_edge_cost(direct_edge_attributes, normalized_weights) + return _compute_weighted_edge_cost( + direct_edge_attributes, normalized_weights + ) - parallel_edge_attributes = extract_parallel_edge_attributes( + parallel_edge_attributes = _extract_parallel_edge_attribute_mappings( networkx_edge_payload ) @@ -119,23 +123,25 @@ def edge_weight( return FALLBACK_EDGE_COST return min( - compute_scalar_edge_cost(edge_attributes, normalized_weights) + _compute_weighted_edge_cost(edge_attributes, normalized_weights) for edge_attributes in parallel_edge_attributes ) return edge_weight -def select_parallel_edge( +def select_parallel_edge_attributes( graph: MultiDiGraphAny, source_node_id: int, target_node_id: int, *, - ranking_key: Callable[[EdgeAttributes], float], -) -> EdgeAttributes | None: + ranking_key: Callable[[EdgeAttributeMap], float], +) -> EdgeAttributeMap | None: """Select one parallel edge according to the provided ranking key.""" parallel_edges_payload = graph.get_edge_data(source_node_id, target_node_id) - parallel_edge_attributes = extract_parallel_edge_attributes(parallel_edges_payload) + parallel_edge_attributes = _extract_parallel_edge_attribute_mappings( + parallel_edges_payload + ) if not parallel_edge_attributes: return None @@ -143,18 +149,18 @@ def select_parallel_edge( return min(parallel_edge_attributes, key=ranking_key) -def select_best_parallel_edge_by_scalar_cost( +def select_lowest_cost_parallel_edge( graph: MultiDiGraphAny, source_node_id: int, target_node_id: int, - normalized_weights: NormalizedRouteObjectiveWeights, -) -> EdgeAttributes | None: + normalized_weights: NormalizedRoutePreferenceWeights, +) -> EdgeAttributeMap | None: """Select the lowest scalar-cost parallel edge for a graph segment.""" - return select_parallel_edge( + return select_parallel_edge_attributes( graph, source_node_id, target_node_id, - ranking_key=lambda edge_attributes: compute_scalar_edge_cost( + ranking_key=lambda edge_attributes: _compute_weighted_edge_cost( edge_attributes, normalized_weights, ), diff --git a/backend/app/geocoding.py b/backend/app/geocoding.py index 7134dab..318893e 100644 --- a/backend/app/geocoding.py +++ b/backend/app/geocoding.py @@ -13,15 +13,15 @@ type JsonObject = dict[str, object] -def reverse_geocode_address( +def reverse_geocode_to_address( longitude: float, latitude: float, *, zoom_level: int = 18, ) -> ReverseGeocodeResponse: """Reverse geocode coordinates to a single-line road + house number string.""" - reverse_url = ox.settings.nominatim_url.rstrip("/") + "/reverse" - request_get = cast("Callable[..., requests.Response]", requests.get) + reverse_endpoint_url = ox.settings.nominatim_url.rstrip("/") + "/reverse" + send_get_request = cast("Callable[..., requests.Response]", requests.get) params: dict[str, str | int | float] = { "format": "jsonv2", @@ -35,8 +35,8 @@ def reverse_geocode_address( "Accept-Language": ox.settings.http_accept_language, } - response = request_get( - reverse_url, + response = send_get_request( + reverse_endpoint_url, params=params, headers=headers, timeout=ox.settings.requests_timeout, @@ -44,18 +44,18 @@ def reverse_geocode_address( ) response.raise_for_status() - payload = cast("object", response.json()) + response_data = cast("object", response.json()) - if not isinstance(payload, dict): + if not isinstance(response_data, dict): return ReverseGeocodeResponse(address="") - payload_object = cast("JsonObject", payload) - raw_address = payload_object.get("address") + payload_object = cast("JsonObject", response_data) + address_payload = payload_object.get("address") - if not isinstance(raw_address, dict): + if not isinstance(address_payload, dict): return ReverseGeocodeResponse(address="") - address_object = cast("JsonObject", raw_address) + address_object = cast("JsonObject", address_payload) road = str(address_object.get("road") or "").strip() house_number = str(address_object.get("house_number") or "").strip() formatted_address = " ".join(part for part in (road, house_number) if part) diff --git a/backend/app/graph_state.py b/backend/app/graph_state.py index e98fb33..4f4170e 100644 --- a/backend/app/graph_state.py +++ b/backend/app/graph_state.py @@ -9,14 +9,14 @@ from shapely.geometry import Point as ShapelyPoint from shapely.geometry import Polygon as ShapelyPolygon -from app.overlays import apply_all_overlays +from app.overlays import load_and_apply_overlays if TYPE_CHECKING: from collections.abc import Callable import geopandas as gpd - from app.models import TransportMode + from app.models import TravelMode from app.typing_aliases import MultiDiGraphAny type BoundaryGeometry = ShapelyPolygon | ShapelyMultiPolygon @@ -26,11 +26,11 @@ class LoadedGraphState: """All in-memory graph artifacts required by API endpoints.""" - bike_graph: MultiDiGraphAny | None = None - walk_graph: MultiDiGraphAny | None = None - bike_edges: gpd.GeoDataFrame | None = None - walk_edges: gpd.GeoDataFrame | None = None - boundary_polygon: BoundaryGeometry | None = None + cycling_graph: MultiDiGraphAny | None = None + walking_graph: MultiDiGraphAny | None = None + cycling_edges: gpd.GeoDataFrame | None = None + walking_edges: gpd.GeoDataFrame | None = None + boundary_geometry: BoundaryGeometry | None = None GRAPH_STATE = LoadedGraphState() @@ -51,7 +51,7 @@ def _graph_to_edge_geodataframe(graph: MultiDiGraphAny) -> gpd.GeoDataFrame: ) -def _graph_from_place( +def _load_graph_from_place( place_name: str, *, network_type: str, @@ -65,7 +65,7 @@ def _graph_from_place( return graph_from_place(place_name, network_type=network_type) -def _geocode_to_gdf(place_name: str) -> gpd.GeoDataFrame: +def _geocode_place_to_geodataframe(place_name: str) -> gpd.GeoDataFrame: """Call OSMnx geocode_to_gdf with a typed return value.""" geocode_to_gdf = cast( "Callable[[str], gpd.GeoDataFrame]", @@ -75,16 +75,16 @@ def _geocode_to_gdf(place_name: str) -> gpd.GeoDataFrame: return geocode_to_gdf(place_name) -def build_edge_geodataframe(graph: MultiDiGraphAny) -> gpd.GeoDataFrame: +def _build_edge_geodataframe(graph: MultiDiGraphAny) -> gpd.GeoDataFrame: """Build edge GeoDataFrame from a graph, ensuring WGS84 CRS.""" edge_geodataframe = _graph_to_edge_geodataframe(graph) return edge_geodataframe.set_crs("EPSG:4326", allow_override=True) -def load_boundary_polygon(place_name: str) -> ShapelyMultiPolygon: +def load_boundary_geometry(place_name: str) -> ShapelyMultiPolygon: """Load and normalize place boundary geometry as a MultiPolygon.""" - boundary_geodataframe = _geocode_to_gdf(place_name) + boundary_geodataframe = _geocode_place_to_geodataframe(place_name) boundary_geometry = boundary_geodataframe.geometry.iloc[0] if isinstance(boundary_geometry, ShapelyPolygon): @@ -105,26 +105,28 @@ def load_graph_state( graph_state: LoadedGraphState, ) -> None: """Load graphs, overlays, edge indexes, and boundaries into state.""" - bike_graph = _graph_from_place(place_name, network_type="bike") - walk_graph = _graph_from_place(place_name, network_type="walk") + bike_graph = _load_graph_from_place(place_name, network_type="bike") + walk_graph = _load_graph_from_place(place_name, network_type="walk") - apply_all_overlays(bike_graph, overlay_directory) - apply_all_overlays(walk_graph, overlay_directory) + load_and_apply_overlays(bike_graph, overlay_directory) + load_and_apply_overlays(walk_graph, overlay_directory) - graph_state.bike_graph = bike_graph - graph_state.walk_graph = walk_graph - graph_state.bike_edges = build_edge_geodataframe(bike_graph) - graph_state.walk_edges = build_edge_geodataframe(walk_graph) - graph_state.boundary_polygon = load_boundary_polygon(place_name) + graph_state.cycling_graph = bike_graph + graph_state.walking_graph = walk_graph + graph_state.cycling_edges = _build_edge_geodataframe(bike_graph) + graph_state.walking_edges = _build_edge_geodataframe(walk_graph) + graph_state.boundary_geometry = load_boundary_geometry(place_name) -def get_graph_for_mode( +def get_graph_for_travel_mode( graph_state: LoadedGraphState, - transport_mode: TransportMode, + travel_mode: TravelMode, ) -> MultiDiGraphAny: """Return the loaded graph for the requested transport mode.""" selected_graph = ( - graph_state.bike_graph if transport_mode == "bike" else graph_state.walk_graph + graph_state.cycling_graph + if travel_mode == "cycling" + else graph_state.walking_graph ) if selected_graph is None: @@ -133,13 +135,15 @@ def get_graph_for_mode( return selected_graph -def get_edges_for_mode( +def get_edge_geodataframe_for_travel_mode( graph_state: LoadedGraphState, - transport_mode: TransportMode, + travel_mode: TravelMode, ) -> gpd.GeoDataFrame: """Return the loaded edge GeoDataFrame for the requested transport mode.""" selected_edges = ( - graph_state.bike_edges if transport_mode == "bike" else graph_state.walk_edges + graph_state.cycling_edges + if travel_mode == "cycling" + else graph_state.walking_edges ) if selected_edges is None: @@ -148,13 +152,13 @@ def get_edges_for_mode( return selected_edges -def validate_point_within_boundary( +def validate_coordinate_within_boundary( graph_state: LoadedGraphState, longitude: float, latitude: float, ) -> None: """Validate that a coordinate is inside the configured boundary.""" - boundary_polygon = graph_state.boundary_polygon + boundary_polygon = graph_state.boundary_geometry if boundary_polygon is None: return diff --git a/backend/app/layer_service.py b/backend/app/layer_service.py index 13fc034..af2867e 100644 --- a/backend/app/layer_service.py +++ b/backend/app/layer_service.py @@ -9,12 +9,12 @@ from shapely.geometry import Polygon as ShapelyPolygon from app.models import ( - LayerFeature, - LayerFeatureCollection, - LayerProperties, - OverlayAttribute, + OverlayFeature, + OverlayFeatureCollection, + OverlayFeatureProperties, + OverlayKey, ) -from app.value_parsing import coerce_float +from app.value_parsing import parse_float_or_default if TYPE_CHECKING: from collections.abc import Callable @@ -22,14 +22,14 @@ import geopandas as gpd from shapely.coords import CoordinateSequence -BOUNDING_BOX_PARTS = 4 +BOUNDING_BOX_COORDINATE_COUNT = 4 -def parse_bounding_box(bounding_box: str) -> tuple[float, float, float, float]: +def parse_bounding_box_string(bounding_box: str) -> tuple[float, float, float, float]: """Parse minLon,minLat,maxLon,maxLat string to numeric bounds.""" parts = [part.strip() for part in bounding_box.split(",")] - if len(parts) != BOUNDING_BOX_PARTS: + if len(parts) != BOUNDING_BOX_COORDINATE_COUNT: raise HTTPException( status_code=400, detail="bbox must be minLon, minLat, maxLon, maxLat", @@ -49,20 +49,20 @@ def parse_bounding_box(bounding_box: str) -> tuple[float, float, float, float]: return min_longitude, min_latitude, max_longitude, max_latitude -def filter_edges_for_layer( +def filter_edges_for_overlay( edge_geodataframe: gpd.GeoDataFrame, *, bounding_box: str | None, - overlay_attribute: OverlayAttribute, - minimum_attribute_value: float, - feature_limit: int, + overlay_key: OverlayKey, + minimum_overlay_value: float, + max_features: int, ) -> gpd.GeoDataFrame: """Filter edges by bbox, overlay value threshold, and row limit.""" filtered_edges = edge_geodataframe if bounding_box is not None: - min_longitude, min_latitude, max_longitude, max_latitude = parse_bounding_box( - bounding_box + min_longitude, min_latitude, max_longitude, max_latitude = ( + parse_bounding_box_string(bounding_box) ) bounding_geometry = ShapelyPolygon.from_bounds( min_longitude, @@ -76,29 +76,29 @@ def filter_edges_for_layer( ) filtered_edges = filtered_edges.iloc[matching_indices] - if overlay_attribute not in filtered_edges.columns: + if overlay_key not in filtered_edges.columns: raise HTTPException( status_code=400, - detail=f"Missing edge attribute '{overlay_attribute}'.", + detail=f"Missing edge attribute '{overlay_key}'.", ) - minimum_value = float(minimum_attribute_value) + minimum_value = float(minimum_overlay_value) filtered_edges = filtered_edges[ - filtered_edges[overlay_attribute].astype(float) >= minimum_value + filtered_edges[overlay_key].astype(float) >= minimum_value ] - if len(filtered_edges) > feature_limit: - filtered_edges = filtered_edges.head(feature_limit) + if len(filtered_edges) > max_features: + filtered_edges = filtered_edges.head(max_features) return filtered_edges -def build_layer_features( +def build_overlay_features( filtered_edges: gpd.GeoDataFrame, - overlay_attribute: OverlayAttribute, -) -> list[LayerFeature]: + overlay_key: OverlayKey, +) -> list[OverlayFeature]: """Convert filtered edge rows into layer GeoJSON features.""" - layer_features: list[LayerFeature] = [] + layer_features: list[OverlayFeature] = [] for row in filtered_edges.itertuples(): geometry = row.geometry @@ -115,15 +115,15 @@ def build_layer_features( Position2D(float(longitude), float(latitude)) for longitude, latitude in coordinates ] - row_to_dict = cast("Callable[[], dict[str, object]]", row._asdict) - row_values = row_to_dict() - overlay_value = coerce_float(row_values.get(overlay_attribute), default=0.0) + row_as_dict = cast("Callable[[], dict[str, object]]", row._asdict) + row_data = row_as_dict() + overlay_value = parse_float_or_default(row_data.get(overlay_key), default=0.0) layer_features.append( - LayerFeature( + OverlayFeature( type="Feature", - properties=LayerProperties( - overlay_attribute=overlay_attribute, + properties=OverlayFeatureProperties( + overlay_key=overlay_key, value=overlay_value, ), geometry=PydanticLineString( @@ -136,22 +136,22 @@ def build_layer_features( return layer_features -def build_layer_feature_collection( +def build_overlay_feature_collection( edge_geodataframe: gpd.GeoDataFrame, *, - overlay_attribute: OverlayAttribute, + overlay_key: OverlayKey, bounding_box: str | None, - minimum_attribute_value: float, - feature_limit: int, -) -> LayerFeatureCollection: + minimum_overlay_value: float, + max_features: int, +) -> OverlayFeatureCollection: """Build layer FeatureCollection response from edge data.""" - filtered_edges = filter_edges_for_layer( + filtered_edges = filter_edges_for_overlay( edge_geodataframe, bounding_box=bounding_box, - overlay_attribute=overlay_attribute, - minimum_attribute_value=minimum_attribute_value, - feature_limit=feature_limit, + overlay_key=overlay_key, + minimum_overlay_value=minimum_overlay_value, + max_features=max_features, ) - layer_features = build_layer_features(filtered_edges, overlay_attribute) + layer_features = build_overlay_features(filtered_edges, overlay_key) - return LayerFeatureCollection(type="FeatureCollection", features=layer_features) + return OverlayFeatureCollection(type="FeatureCollection", features=layer_features) diff --git a/backend/app/main.py b/backend/app/main.py index fd09c8d..000fb49 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -12,29 +12,29 @@ from shapely.geometry import mapping from starlette.middleware.cors import CORSMiddleware -from app.geocoding import reverse_geocode_address +from app.geocoding import reverse_geocode_to_address from app.graph_state import ( GRAPH_STATE, - get_edges_for_mode, + get_edge_geodataframe_for_travel_mode, load_graph_state, ) -from app.layer_service import build_layer_feature_collection +from app.layer_service import build_overlay_feature_collection from app.models import ( AddressRouteRequest, BoundaryFeature, BoundaryFeatureCollection, BoundaryMeta, BoundaryProperties, - CoordinateRouteRequest, - LayerFeatureCollection, - OverlayAttribute, + CoordinatesRouteRequest, + OverlayFeatureCollection, + OverlayKey, ReverseGeocodeResponse, - RouteComputationOptions, RouteCoordinates, RouteFeatureCollection, - RouteObjectiveWeights, - RouteSelectionMethod, - TransportMode, + RouteOptimizationMethod, + RoutePlanningOptions, + RoutePreferenceWeights, + TravelMode, ) from app.route_planner import build_route_feature_collection @@ -45,7 +45,7 @@ OVERLAY_DIRECTORY = "data/overlays" -def _normalize_boundary_polygon(boundary_geometry: object) -> ShapelyMultiPolygon: +def _as_multipolygon(boundary_geometry: object) -> ShapelyMultiPolygon: """Normalize boundary geometry to a MultiPolygon.""" if isinstance(boundary_geometry, ShapelyPolygon): return ShapelyMultiPolygon([boundary_geometry]) @@ -79,7 +79,7 @@ async def lifespan(_app: FastAPI) -> AsyncGenerator[None]: ) -def _build_route_from_address_request( +def _compute_route_from_address_request( request: AddressRouteRequest, ) -> RouteFeatureCollection: """Build route response for address-based requests.""" @@ -100,13 +100,13 @@ def _build_route_from_address_request( destination_longitude=destination_longitude, destination_latitude=destination_latitude, ), - transport_mode=request.transport_mode, + travel_mode=request.travel_mode, route_options=request.route_options, ) -def _build_route_from_coordinate_request( - request: CoordinateRouteRequest, +def _compute_route_from_coordinate_request( + request: CoordinatesRouteRequest, ) -> RouteFeatureCollection: """Build route response for coordinate-based requests.""" raw_origin_longitude, raw_origin_latitude = request.origin.coordinates[:2] @@ -122,13 +122,13 @@ def _build_route_from_coordinate_request( destination_longitude=float(raw_destination_longitude), destination_latitude=float(raw_destination_latitude), ), - transport_mode=request.transport_mode, + travel_mode=request.travel_mode, route_options=request.route_options, ) @dataclass(frozen=True, slots=True) -class ParetoRoutingLimits: +class ParetoSearchLimits: """Upper bounds controlling the pareto label-setting search.""" max_routes: int @@ -136,54 +136,54 @@ class ParetoRoutingLimits: max_total_labels: int -def build_route_objective_weights( - scenic: Annotated[int, Query(ge=0, le=100)] = 0, - avoid_snow: Annotated[int, Query(ge=0, le=100)] = 0, - avoid_uphill: Annotated[int, Query(ge=0, le=100)] = 0, -) -> RouteObjectiveWeights: +def build_route_preference_weights( + scenic_weight: Annotated[int, Query(ge=0, le=100)] = 0, + snow_free_weight: Annotated[int, Query(ge=0, le=100)] = 0, + flat_weight: Annotated[int, Query(ge=0, le=100)] = 0, +) -> RoutePreferenceWeights: """Build route objective weights from GET query parameters.""" - return RouteObjectiveWeights( - scenic=scenic, - avoid_snow=avoid_snow, - avoid_uphill=avoid_uphill, + return RoutePreferenceWeights( + scenic_weight=scenic_weight, + snow_free_weight=snow_free_weight, + flat_weight=flat_weight, ) -def build_pareto_routing_limits( +def build_pareto_search_limits( pareto_max_routes: Annotated[int, Query(ge=1, le=25)] = 8, pareto_max_labels_per_node: Annotated[int, Query(ge=5, le=200)] = 40, pareto_max_total_labels: Annotated[int, Query(ge=1_000, le=500_000)] = 50_000, -) -> ParetoRoutingLimits: +) -> ParetoSearchLimits: """Build pareto-routing search limits from GET query parameters.""" - return ParetoRoutingLimits( + return ParetoSearchLimits( max_routes=pareto_max_routes, max_labels_per_node=pareto_max_labels_per_node, max_total_labels=pareto_max_total_labels, ) -def build_route_computation_options( - route_objective_weights: Annotated[ - RouteObjectiveWeights, - Depends(build_route_objective_weights), +def build_route_planning_options( + route_preference_weights: Annotated[ + RoutePreferenceWeights, + Depends(build_route_preference_weights), ], - pareto_routing_limits: Annotated[ - ParetoRoutingLimits, - Depends(build_pareto_routing_limits), + pareto_search_limits: Annotated[ + ParetoSearchLimits, + Depends(build_pareto_search_limits), ], - route_selection_method: Annotated[RouteSelectionMethod, Query()] = "shortest", -) -> RouteComputationOptions: + route_optimization_method: Annotated[RouteOptimizationMethod, Query()] = "shortest", +) -> RoutePlanningOptions: """Build route computation options from GET query parameters.""" - return RouteComputationOptions( - route_selection_method=route_selection_method, - objective_weights=route_objective_weights, - pareto_max_routes=pareto_routing_limits.max_routes, - pareto_max_labels_per_node=pareto_routing_limits.max_labels_per_node, - pareto_max_total_labels=pareto_routing_limits.max_total_labels, + return RoutePlanningOptions( + route_optimization_method=route_optimization_method, + preference_weights=route_preference_weights, + pareto_max_routes=pareto_search_limits.max_routes, + pareto_max_labels_per_node=pareto_search_limits.max_labels_per_node, + pareto_max_total_labels=pareto_search_limits.max_total_labels, ) -def build_route_coordinates_from_query( +def build_route_coordinates_from_params( origin_longitude: float, origin_latitude: float, destination_longitude: float, @@ -198,39 +198,39 @@ def build_route_coordinates_from_query( ) -def build_address_route_request_from_query( - transport_mode: TransportMode, +def build_address_route_request_from_params( + travel_mode: TravelMode, origin: str, destination: str, route_options: Annotated[ - RouteComputationOptions, - Depends(build_route_computation_options), + RoutePlanningOptions, + Depends(build_route_planning_options), ], ) -> AddressRouteRequest: """Build an address route request from GET query parameters.""" return AddressRouteRequest( - transport_mode=transport_mode, + travel_mode=travel_mode, origin=origin, destination=destination, route_options=route_options, ) -def build_coordinate_route_request_from_query( - transport_mode: TransportMode, +def build_coordinate_route_request_from_params( + travel_mode: TravelMode, route_coordinates: Annotated[ RouteCoordinates, - Depends(build_route_coordinates_from_query), + Depends(build_route_coordinates_from_params), ], route_options: Annotated[ - RouteComputationOptions, - Depends(build_route_computation_options), + RoutePlanningOptions, + Depends(build_route_planning_options), ], -) -> CoordinateRouteRequest: +) -> CoordinatesRouteRequest: """Build a coordinate route request from GET query parameters.""" - return CoordinateRouteRequest.model_validate( + return CoordinatesRouteRequest.model_validate( { - "transport_mode": transport_mode, + "travel_mode": travel_mode, "origin": { "type": "Point", "coordinates": [ @@ -250,35 +250,35 @@ def build_coordinate_route_request_from_query( ) -@app.get("/layers", response_model=LayerFeatureCollection) -def list_layers( - overlay_attribute: OverlayAttribute, - transport_mode: TransportMode, +@app.get("/layers", response_model=OverlayFeatureCollection) +def list_overlay_features( + overlay_key: OverlayKey, + travel_mode: TravelMode, bounding_box: str | None = None, minimum_value: float = 0.01, max_features: int = 20_000, -) -> LayerFeatureCollection: +) -> OverlayFeatureCollection: """Get overlay layer features for the selected transport network.""" - edge_geodataframe = get_edges_for_mode(GRAPH_STATE, transport_mode) + edge_geodataframe = get_edge_geodataframe_for_travel_mode(GRAPH_STATE, travel_mode) - return build_layer_feature_collection( + return build_overlay_feature_collection( edge_geodataframe, - overlay_attribute=overlay_attribute, + overlay_key=overlay_key, bounding_box=bounding_box, - minimum_attribute_value=minimum_value, - feature_limit=max_features, + minimum_overlay_value=minimum_value, + max_features=max_features, ) @app.get("/boundaries/current", response_model=BoundaryFeatureCollection) def get_current_boundary() -> BoundaryFeatureCollection: """Get boundary geometry for the loaded routing area.""" - boundary_geometry = GRAPH_STATE.boundary_polygon + boundary_geometry = GRAPH_STATE.boundary_geometry if boundary_geometry is None: raise HTTPException(status_code=500, detail="Graph not loaded.") - boundary_polygon = _normalize_boundary_polygon(boundary_geometry) + boundary_polygon = _as_multipolygon(boundary_geometry) boundary_geometry = PydanticMultiPolygon.model_validate(mapping(boundary_polygon)) @@ -296,25 +296,25 @@ def get_current_boundary() -> BoundaryFeatureCollection: @app.get("/routes/by-address", response_model=RouteFeatureCollection) -def create_route_from_address( +def compute_route_by_address( request: Annotated[ AddressRouteRequest, - Depends(build_address_route_request_from_query), + Depends(build_address_route_request_from_params), ], ) -> RouteFeatureCollection: """Compute a route from address inputs.""" - return _build_route_from_address_request(request) + return _compute_route_from_address_request(request) @app.get("/routes/by-coordinates", response_model=RouteFeatureCollection) -def create_route_from_coordinates( +def compute_route_by_coordinates( request: Annotated[ - CoordinateRouteRequest, - Depends(build_coordinate_route_request_from_query), + CoordinatesRouteRequest, + Depends(build_coordinate_route_request_from_params), ], ) -> RouteFeatureCollection: """Compute a route from coordinate inputs.""" - return _build_route_from_coordinate_request(request) + return _compute_route_from_coordinate_request(request) @app.get("/geocoding/reverse") @@ -324,4 +324,4 @@ def reverse_geocode( zoom_level: int = 18, ) -> ReverseGeocodeResponse: """Get the nearest address for the given coordinates.""" - return reverse_geocode_address(longitude, latitude, zoom_level=zoom_level) + return reverse_geocode_to_address(longitude, latitude, zoom_level=zoom_level) diff --git a/backend/app/models.py b/backend/app/models.py index 43076ce..44cd1f6 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -8,34 +8,34 @@ from geojson_pydantic import Point as PydanticPoint # noqa: TC002 from pydantic import BaseModel, ConfigDict, Field -TransportMode = Literal["bike", "walk"] -OverlayAttribute = Literal["snow", "scenic", "uphill"] -RouteSelectionMethod = Literal["shortest", "weighted", "pareto"] +TravelMode = Literal["walking", "cycling"] +OverlayKey = Literal["snow", "scenic", "hills"] +RouteOptimizationMethod = Literal["shortest", "weighted", "pareto"] -OVERLAY_ATTRIBUTE_NAMES: tuple[OverlayAttribute, ...] = ("snow", "scenic", "uphill") +OVERLAY_KEYS: tuple[OverlayKey, ...] = ("snow", "scenic", "hills") -class RouteObjectiveWeights(BaseModel): +class RoutePreferenceWeights(BaseModel): """User-defined weights for route optimization objectives.""" - scenic: int = Field(default=0, ge=0, le=100) - avoid_snow: int = Field(default=0, ge=0, le=100) - avoid_uphill: int = Field(default=0, ge=0, le=100) + scenic_weight: int = Field(default=0, ge=0, le=100) + snow_free_weight: int = Field(default=0, ge=0, le=100) + flat_weight: int = Field(default=0, ge=0, le=100) -def build_default_route_objective_weights() -> RouteObjectiveWeights: +def build_default_route_preference_weights() -> RoutePreferenceWeights: """Build default objective weights for request options.""" - return RouteObjectiveWeights() + return RoutePreferenceWeights() -class RouteComputationOptions(BaseModel): +class RoutePlanningOptions(BaseModel): """Options controlling how routes are computed.""" model_config: ClassVar[ConfigDict] = ConfigDict(populate_by_name=True) - route_selection_method: RouteSelectionMethod = Field(default="shortest") - objective_weights: RouteObjectiveWeights = Field( - default_factory=build_default_route_objective_weights, + route_optimization_method: RouteOptimizationMethod = Field(default="shortest") + preference_weights: RoutePreferenceWeights = Field( + default_factory=build_default_route_preference_weights, ) pareto_max_routes: int = Field(default=8, ge=1, le=25) @@ -43,20 +43,20 @@ class RouteComputationOptions(BaseModel): pareto_max_total_labels: int = Field(default=50_000, ge=1_000, le=500_000) -def build_default_route_options() -> RouteComputationOptions: +def build_default_route_options() -> RoutePlanningOptions: """Build default routing options for route requests.""" - return RouteComputationOptions() + return RoutePlanningOptions() -class CoordinateRouteRequest(BaseModel): +class CoordinatesRouteRequest(BaseModel): """Route request where origin and destination are coordinates.""" model_config: ClassVar[ConfigDict] = ConfigDict(populate_by_name=True) - transport_mode: TransportMode + travel_mode: TravelMode origin: PydanticPoint destination: PydanticPoint - route_options: RouteComputationOptions = Field( + route_options: RoutePlanningOptions = Field( default_factory=build_default_route_options ) @@ -66,25 +66,25 @@ class AddressRouteRequest(BaseModel): model_config: ClassVar[ConfigDict] = ConfigDict(populate_by_name=True) - transport_mode: TransportMode + travel_mode: TravelMode origin: str destination: str - route_options: RouteComputationOptions = Field( + route_options: RoutePlanningOptions = Field( default_factory=build_default_route_options ) -class RouteStepResponse(BaseModel): +class RouteStepSummary(BaseModel): """A high-level navigation step on the route.""" street: str distance: float segment_index_from: int segment_index_to: int - objective_costs: RouteObjectiveCostBreakdown + penalty_breakdown: RoutePenaltyBreakdown -class RouteObjectiveCostBreakdown(BaseModel): +class RoutePenaltyBreakdown(BaseModel): """Objective-aligned route cost totals.""" distance: float @@ -98,8 +98,8 @@ class RouteProperties(BaseModel): route_index: int = Field(description="Zero-based route index within the response") distance: float = Field(description="Route distance in metres") - steps: list[RouteStepResponse] - objective_costs: RouteObjectiveCostBreakdown | None = None + steps: list[RouteStepSummary] + penalty_breakdown: RoutePenaltyBreakdown | None = None pareto_rank: int | None = None selection_score: float | None = None @@ -117,7 +117,7 @@ class RouteMeta(BaseModel): origin: PydanticPoint destination: PydanticPoint - route_selection_method: RouteSelectionMethod + route_optimization_method: RouteOptimizationMethod route_count: int recommended_route_index: int @@ -158,26 +158,26 @@ class BoundaryFeatureCollection(BaseModel): meta: BoundaryMeta -class LayerProperties(BaseModel): +class OverlayFeatureProperties(BaseModel): """Properties attached to a layer feature.""" - overlay_attribute: OverlayAttribute + overlay_key: OverlayKey value: float -class LayerFeature(BaseModel): +class OverlayFeature(BaseModel): """Layer feature geometry and properties.""" type: Literal["Feature"] geometry: PydanticLineString - properties: LayerProperties + properties: OverlayFeatureProperties -class LayerFeatureCollection(BaseModel): +class OverlayFeatureCollection(BaseModel): """Layer FeatureCollection.""" type: Literal["FeatureCollection"] - features: list[LayerFeature] + features: list[OverlayFeature] class ReverseGeocodeResponse(BaseModel): @@ -187,7 +187,7 @@ class ReverseGeocodeResponse(BaseModel): @dataclass(frozen=True, slots=True) -class RouteStep: +class AggregatedRouteStep: """A grouped step along contiguous segments with the same street.""" street: str @@ -200,12 +200,12 @@ class RouteStep: @dataclass(frozen=True, slots=True) -class NormalizedRouteObjectiveWeights: +class NormalizedRoutePreferenceWeights: """Objective weights scaled to [0.0, 1.0].""" - scenic: float - avoid_snow: float - avoid_uphill: float + scenic_weight: float + snow_free_weight: float + flat_weight: float @dataclass(frozen=True, slots=True) @@ -218,14 +218,14 @@ class RouteCoordinates: destination_latitude: float -type RouteCostVector = tuple[float, float, float, float] +type ParetoCostVector = tuple[float, float, float, float] @dataclass(slots=True) -class ParetoPathLabel: +class ParetoSearchLabel: """A label in the Martins multi-objective shortest-path search.""" node_id: int - cost_vector: RouteCostVector + cost_vector: ParetoCostVector previous_label_id: int | None previous_edge_key: tuple[int, int, int] | None diff --git a/backend/app/overlays.py b/backend/app/overlays.py index a9a480c..3ea038e 100644 --- a/backend/app/overlays.py +++ b/backend/app/overlays.py @@ -13,41 +13,42 @@ from shapely.geometry import Point as ShapelyPoint from shapely.geometry import Polygon as ShapelyPolygon -from app.models import OVERLAY_ATTRIBUTE_NAMES, OverlayAttribute -from app.value_parsing import coerce_float +from app.models import OVERLAY_KEYS, OverlayKey +from app.value_parsing import parse_float_or_default if TYPE_CHECKING: from collections.abc import Callable - from app.typing_aliases import EdgeAttributes, MultiDiGraphAny + from app.typing_aliases import EdgeAttributeMap, MultiDiGraphAny -type PolygonIndexArray = npt.NDArray[np.int64] +type OverlayPolygonIndexArray = npt.NDArray[np.int64] +type MergeStrategy = Literal["max", "last"] @dataclass(frozen=True, slots=True) -class OverlayApplicationContext: +class OverlayAssignmentContext: """Shared context for applying one overlay attribute across graph edges.""" graph: MultiDiGraphAny overlay_tree: STRtree overlay_polygons: list[ShapelyPolygon] polygon_value_by_id: dict[int, float] - overlay_attribute: OverlayAttribute - combination_mode: Literal["max", "last"] + overlay_key: OverlayKey + merge_strategy: MergeStrategy -def _overlay_file_path(relative_overlay_path: str) -> Path: +def _resolve_overlay_path(relative_overlay_path: str) -> Path: """Resolve an overlay path relative to the backend directory.""" backend_directory = Path(__file__).resolve().parent.parent return backend_directory / relative_overlay_path -def get_edge_linestring( +def get_edge_geometry_linestring( graph: MultiDiGraphAny, source_node_id: int, target_node_id: int, - edge_attributes: EdgeAttributes, + edge_attributes: EdgeAttributeMap, ) -> ShapelyLineString: """Return edge geometry or fallback to a straight node-to-node segment.""" geometry = edge_attributes.get("geometry") @@ -58,10 +59,10 @@ def get_edge_linestring( start_node = graph.nodes[source_node_id] end_node = graph.nodes[target_node_id] - start_longitude = coerce_float(start_node.get("x"), default=0.0) - start_latitude = coerce_float(start_node.get("y"), default=0.0) - end_longitude = coerce_float(end_node.get("x"), default=0.0) - end_latitude = coerce_float(end_node.get("y"), default=0.0) + start_longitude = parse_float_or_default(start_node.get("x"), default=0.0) + start_latitude = parse_float_or_default(start_node.get("y"), default=0.0) + end_longitude = parse_float_or_default(end_node.get("x"), default=0.0) + end_latitude = parse_float_or_default(end_node.get("y"), default=0.0) return ShapelyLineString( [ @@ -71,11 +72,11 @@ def get_edge_linestring( ) -def load_overlay_polygons( +def load_overlay_geometries( relative_overlay_path: str, ) -> tuple[list[ShapelyPolygon], list[float]]: """Load overlay polygons and corresponding values from GeoJSON.""" - overlay_path = _overlay_file_path(relative_overlay_path) + overlay_path = _resolve_overlay_path(relative_overlay_path) read_file = cast( "Callable[..., gpd.GeoDataFrame]", gpd.read_file, @@ -105,7 +106,7 @@ def load_overlay_polygons( overlay_raw_values, strict=True, ): - overlay_value = coerce_float(raw_value, default=0.0) + overlay_value = parse_float_or_default(raw_value, default=0.0) if isinstance(geometry, ShapelyPolygon): polygons.append(geometry) @@ -127,16 +128,18 @@ def load_overlay_polygons( return polygons, values -def _collect_covering_overlay_values( +def _collect_midpoint_overlay_values( midpoint: ShapelyPoint, - candidate_indices: PolygonIndexArray, + candidate_polygon_indices: OverlayPolygonIndexArray, overlay_polygons: list[ShapelyPolygon], polygon_value_by_id: dict[int, float], ) -> list[float]: """Collect overlay values of polygons covering the midpoint.""" overlay_values: list[float] = [] - candidate_index_list = cast("list[int]", candidate_indices.astype(int).tolist()) + candidate_index_list = cast( + "list[int]", candidate_polygon_indices.astype(int).tolist() + ) for candidate_index in candidate_index_list: polygon = overlay_polygons[int(candidate_index)] @@ -147,62 +150,62 @@ def _collect_covering_overlay_values( return overlay_values -def _apply_overlay_to_edge( - context: OverlayApplicationContext, +def _apply_overlay_key_to_edge( + context: OverlayAssignmentContext, source_node_id: int, target_node_id: int, - edge_attributes: EdgeAttributes, + edge_attributes: EdgeAttributeMap, ) -> None: """Apply overlay value to a single edge when midpoint intersects polygons.""" - edge_line = get_edge_linestring( + edge_linestring = get_edge_geometry_linestring( graph=context.graph, source_node_id=source_node_id, target_node_id=target_node_id, edge_attributes=edge_attributes, ) - midpoint = edge_line.interpolate(0.5, normalized=True) + edge_midpoint = edge_linestring.interpolate(0.5, normalized=True) - candidate_indices = context.overlay_tree.query(midpoint) + candidate_polygon_indices = context.overlay_tree.query(edge_midpoint) - if len(candidate_indices) == 0: + if len(candidate_polygon_indices) == 0: return - covering_values = _collect_covering_overlay_values( - midpoint=midpoint, - candidate_indices=candidate_indices, + matching_overlay_values = _collect_midpoint_overlay_values( + midpoint=edge_midpoint, + candidate_polygon_indices=candidate_polygon_indices, overlay_polygons=context.overlay_polygons, polygon_value_by_id=context.polygon_value_by_id, ) - if not covering_values: + if not matching_overlay_values: return overlay_value = ( - max(covering_values) - if context.combination_mode == "max" - else covering_values[-1] + max(matching_overlay_values) + if context.merge_strategy == "max" + else matching_overlay_values[-1] ) - edge_attributes[context.overlay_attribute] = float(overlay_value) + edge_attributes[context.overlay_key] = float(overlay_value) -def initialize_overlay_attributes(graph: MultiDiGraphAny) -> None: +def initialize_edge_overlay_values(graph: MultiDiGraphAny) -> None: """Ensure every edge has all overlay attributes initialized to zero.""" for source_node_id, target_node_id, edge_attributes in graph.edges(data=True): _ = source_node_id, target_node_id - for overlay_attribute in OVERLAY_ATTRIBUTE_NAMES: + for overlay_attribute in OVERLAY_KEYS: edge_attributes.setdefault(overlay_attribute, 0.0) -def apply_overlay_attribute( +def apply_overlay_key( graph: MultiDiGraphAny, - overlay_attribute: OverlayAttribute, + overlay_key: OverlayKey, overlay_polygons: list[ShapelyPolygon], overlay_values: list[float], - combination_mode: Literal["max", "last"] = "max", + merge_strategy: MergeStrategy = "max", ) -> None: """Apply one overlay attribute across all graph edges.""" - context = OverlayApplicationContext( + context = OverlayAssignmentContext( graph=graph, overlay_tree=STRtree(overlay_polygons), overlay_polygons=overlay_polygons, @@ -210,12 +213,12 @@ def apply_overlay_attribute( id(polygon): value for polygon, value in zip(overlay_polygons, overlay_values, strict=True) }, - overlay_attribute=overlay_attribute, - combination_mode=combination_mode, + overlay_key=overlay_key, + merge_strategy=merge_strategy, ) for source_node_id, target_node_id, edge_attributes in graph.edges(data=True): - _apply_overlay_to_edge( + _apply_overlay_key_to_edge( context=context, source_node_id=source_node_id, target_node_id=target_node_id, @@ -223,18 +226,18 @@ def apply_overlay_attribute( ) -def apply_all_overlays(graph: MultiDiGraphAny, overlay_directory: str) -> None: +def load_and_apply_overlays(graph: MultiDiGraphAny, overlay_directory: str) -> None: """Load and apply all overlay files to a graph.""" - initialize_overlay_attributes(graph) + initialize_edge_overlay_values(graph) - for overlay_attribute in OVERLAY_ATTRIBUTE_NAMES: - polygons, values = load_overlay_polygons( + for overlay_attribute in OVERLAY_KEYS: + polygons, values = load_overlay_geometries( f"{overlay_directory}/{overlay_attribute}.json" ) - apply_overlay_attribute( + apply_overlay_key( graph=graph, - overlay_attribute=overlay_attribute, + overlay_key=overlay_attribute, overlay_polygons=polygons, overlay_values=values, - combination_mode="max", + merge_strategy="max", ) diff --git a/backend/app/route_planner.py b/backend/app/route_planner.py index ef6a57c..ee13c2b 100644 --- a/backend/app/route_planner.py +++ b/backend/app/route_planner.py @@ -16,52 +16,52 @@ from app.costs import ( FALLBACK_EDGE_COST, - build_networkx_weight_function, - compute_edge_objective_components, - normalize_route_objective_weights, - select_best_parallel_edge_by_scalar_cost, - select_parallel_edge, + build_weighted_edge_cost_function, + compute_edge_cost_components, + normalize_route_preference_weights, + select_lowest_cost_parallel_edge, + select_parallel_edge_attributes, ) from app.graph_state import ( LoadedGraphState, - get_graph_for_mode, - validate_point_within_boundary, + get_graph_for_travel_mode, + validate_coordinate_within_boundary, ) from app.models import ( - ParetoPathLabel, - RouteComputationOptions, + AggregatedRouteStep, + ParetoCostVector, + ParetoSearchLabel, RouteCoordinates, - RouteCostVector, RouteFeature, RouteFeatureCollection, RouteMeta, - RouteObjectiveCostBreakdown, - RouteObjectiveWeights, + RoutePenaltyBreakdown, + RoutePlanningOptions, + RoutePreferenceWeights, RouteProperties, - RouteStep, - RouteStepResponse, - TransportMode, + RouteStepSummary, + TravelMode, ) -from app.value_parsing import coerce_float +from app.value_parsing import parse_float_or_default if TYPE_CHECKING: - from app.typing_aliases import EdgeAttributes, MultiDiGraphAny + from app.typing_aliases import EdgeAttributeMap, MultiDiGraphAny -type EdgeSelector = Callable[[int, int], EdgeAttributes | None] +type EdgeSelector = Callable[[int, int], EdgeAttributeMap | None] type ShortestPathFunction = Callable[..., list[int]] type PathWeightFunction = Callable[..., float | int] type NearestNodeFunction = Callable[..., int] @dataclass(frozen=True, slots=True) -class ResolvedRouteCandidate: +class RouteCandidate: """A fully resolved route candidate ready for response serialization.""" - path_node_ids: list[int] - segment_edge_attributes: list[EdgeAttributes] - total_cost_vector: RouteCostVector - selection_score: float + node_path: list[int] + path_edge_attributes: list[EdgeAttributeMap] + total_cost_vector: ParetoCostVector + ranking_score: float pareto_rank: int | None = None @@ -80,7 +80,7 @@ def _coerce_street_component(value: object) -> str | None: return str(scalar_value) -def _resolve_street_name(edge_attributes: EdgeAttributes) -> str: +def _resolve_street_name(edge_attributes: EdgeAttributeMap) -> str: """Resolve user-facing street label from OSM edge attributes.""" street_name = _coerce_street_component(edge_attributes.get("name")) street_reference = _coerce_street_component(edge_attributes.get("ref")) @@ -99,39 +99,29 @@ def _resolve_street_name(edge_attributes: EdgeAttributes) -> str: return "Unnamed road" -def select_shortest_length_edge( +def _select_shortest_parallel_edge( graph: MultiDiGraphAny, source_node_id: int, target_node_id: int, -) -> EdgeAttributes | None: +) -> EdgeAttributeMap | None: """Select the shortest parallel edge for a node pair.""" - return select_parallel_edge( + return select_parallel_edge_attributes( graph, source_node_id, target_node_id, - ranking_key=lambda edge_attributes: coerce_float( + ranking_key=lambda edge_attributes: parse_float_or_default( edge_attributes.get("length"), default=FALLBACK_EDGE_COST, ), ) -def build_route_steps( +def _resolve_path_edge_attributes( path_node_ids: list[int], edge_selector: EdgeSelector, -) -> list[RouteStep]: - """Build grouped route steps by merging adjacent segments on the same street.""" - segment_edge_attributes = resolve_route_segment_edges(path_node_ids, edge_selector) - - return build_route_steps_from_edges(segment_edge_attributes) - - -def resolve_route_segment_edges( - path_node_ids: list[int], - edge_selector: EdgeSelector, -) -> list[EdgeAttributes]: +) -> list[EdgeAttributeMap]: """Resolve edge attributes for each segment in a node path.""" - segment_edge_attributes: list[EdgeAttributes] = [] + segment_edge_attributes: list[EdgeAttributeMap] = [] for source_node_id, target_node_id in pairwise(path_node_ids): edge_attributes = edge_selector(source_node_id, target_node_id) @@ -149,12 +139,12 @@ def resolve_route_segment_edges( return segment_edge_attributes -def build_route_steps_from_edges( - segment_edge_attributes: list[EdgeAttributes], -) -> list[RouteStep]: +def _aggregate_route_steps_from_edges( + segment_edge_attributes: list[EdgeAttributeMap], +) -> list[AggregatedRouteStep]: """Build grouped route steps from an ordered edge sequence.""" - route_steps: list[RouteStep] = [] - current_step: RouteStep | None = None + route_steps: list[AggregatedRouteStep] = [] + current_step: AggregatedRouteStep | None = None for segment_index, edge_attributes in enumerate(segment_edge_attributes): ( @@ -162,11 +152,11 @@ def build_route_steps_from_edges( snow_penalty, uphill_penalty, scenic_penalty, - ) = compute_edge_objective_components(edge_attributes) + ) = compute_edge_cost_components(edge_attributes) street_name = _resolve_street_name(edge_attributes) if current_step is None: - current_step = RouteStep( + current_step = AggregatedRouteStep( street=street_name, distance=segment_distance, segment_index_from=segment_index, @@ -178,7 +168,7 @@ def build_route_steps_from_edges( continue if street_name == current_step.street: - current_step = RouteStep( + current_step = AggregatedRouteStep( street=current_step.street, distance=current_step.distance + segment_distance, segment_index_from=current_step.segment_index_from, @@ -190,7 +180,7 @@ def build_route_steps_from_edges( continue route_steps.append(current_step) - current_step = RouteStep( + current_step = AggregatedRouteStep( street=street_name, distance=segment_distance, segment_index_from=segment_index, @@ -206,25 +196,25 @@ def build_route_steps_from_edges( return route_steps -def compute_weighted_shortest_path( +def _compute_weighted_shortest_path( graph: MultiDiGraphAny, origin_node_id: int, destination_node_id: int, - route_objective_weights: RouteObjectiveWeights, + route_preference_weights: RoutePreferenceWeights, ) -> list[int]: """Compute weighted shortest path using objective-based edge costs.""" - normalized_weights = normalize_route_objective_weights(route_objective_weights) + normalized_weights = normalize_route_preference_weights(route_preference_weights) shortest_path = cast("ShortestPathFunction", nx.shortest_path) return shortest_path( graph, source=origin_node_id, target=destination_node_id, - weight=build_networkx_weight_function(normalized_weights), + weight=build_weighted_edge_cost_function(normalized_weights), ) -def compute_shortest_distance_path( +def _compute_shortest_distance_path( graph: MultiDiGraphAny, origin_node_id: int, destination_node_id: int, @@ -241,8 +231,8 @@ def compute_shortest_distance_path( def dominates_cost_vector( - candidate_cost_vector: RouteCostVector, - other_cost_vector: RouteCostVector, + candidate_cost_vector: ParetoCostVector, + other_cost_vector: ParetoCostVector, ) -> bool: """Return whether one route cost vector Pareto-dominates another.""" less_equal_all = all( @@ -265,10 +255,10 @@ def dominates_cost_vector( return less_equal_all and less_than_any -def is_dominated_by_existing_labels( - labels: list[ParetoPathLabel], +def _is_cost_vector_dominated( + labels: list[ParetoSearchLabel], existing_label_ids: list[int], - candidate_cost_vector: RouteCostVector, + candidate_cost_vector: ParetoCostVector, ) -> bool: """Return whether any existing label dominates the candidate label.""" return any( @@ -280,11 +270,11 @@ def is_dominated_by_existing_labels( ) -def build_surviving_label_ids( - labels: list[ParetoPathLabel], +def _prune_label_ids_for_node( + labels: list[ParetoSearchLabel], existing_label_ids: list[int], candidate_label_id: int, - candidate_cost_vector: RouteCostVector, + candidate_cost_vector: ParetoCostVector, max_labels_per_node: int, ) -> list[int]: """Filter dominated labels and enforce the per-node label cap.""" @@ -314,14 +304,14 @@ def sort_key_for_label_id(label_id: int) -> tuple[float, float]: return surviving_label_ids[:max_labels_per_node] -def filter_nondominated_destination_label_ids( - labels: list[ParetoPathLabel], - target_label_ids: list[int], +def _select_nondominated_destination_label_ids( + labels: list[ParetoSearchLabel], + destination_label_ids: list[int], ) -> list[int]: """Return the nondominated label IDs among destination labels.""" nondominated_label_ids: list[int] = [] - for label_id in target_label_ids: + for label_id in destination_label_ids: candidate_cost_vector = labels[label_id].cost_vector if any( @@ -329,7 +319,7 @@ def filter_nondominated_destination_label_ids( labels[other_label_id].cost_vector, candidate_cost_vector, ) - for other_label_id in target_label_ids + for other_label_id in destination_label_ids if other_label_id != label_id ): continue @@ -339,19 +329,19 @@ def filter_nondominated_destination_label_ids( return nondominated_label_ids -def calculate_pareto_frontier_labels( +def run_pareto_label_search( graph: MultiDiGraphAny, origin_node_id: int, destination_node_id: int, *, max_labels_per_node: int, max_total_labels: int, -) -> tuple[list[ParetoPathLabel], list[int]]: +) -> tuple[list[ParetoSearchLabel], list[int]]: """Run Martins' label-setting algorithm and return nondominated target labels.""" - labels: list[ParetoPathLabel] = [] + labels: list[ParetoSearchLabel] = [] labels_at: dict[int, list[int]] = {origin_node_id: []} - start_label = ParetoPathLabel( + start_label = ParetoSearchLabel( node_id=origin_node_id, cost_vector=(0.0, 0.0, 0.0, 0.0), previous_label_id=None, @@ -379,7 +369,7 @@ def is_active_label(node_id: int, label_id: int) -> bool: source_node_id = current_label.node_id outgoing_edges = cast( - "list[tuple[int, int, int, EdgeAttributes]]", + "list[tuple[int, int, int, EdgeAttributeMap]]", list( graph.out_edges( source_node_id, @@ -390,7 +380,7 @@ def is_active_label(node_id: int, label_id: int) -> bool: ) for _, target_node_id, parallel_edge_key, edge_attributes in outgoing_edges: - edge_cost_vector = compute_edge_objective_components(edge_attributes) + edge_cost_vector = compute_edge_cost_components(edge_attributes) new_cost_vector = ( current_label.cost_vector[0] + edge_cost_vector[0], current_label.cost_vector[1] + edge_cost_vector[1], @@ -400,7 +390,7 @@ def is_active_label(node_id: int, label_id: int) -> bool: existing_label_ids = labels_at.get(target_node_id, []) - if is_dominated_by_existing_labels( + if _is_cost_vector_dominated( labels, existing_label_ids, new_cost_vector, @@ -408,7 +398,7 @@ def is_active_label(node_id: int, label_id: int) -> bool: continue candidate_label_id = len(labels) - surviving_label_ids = build_surviving_label_ids( + surviving_label_ids = _prune_label_ids_for_node( labels, existing_label_ids, candidate_label_id, @@ -419,7 +409,7 @@ def is_active_label(node_id: int, label_id: int) -> bool: if candidate_label_id not in surviving_label_ids: continue - new_label = ParetoPathLabel( + new_label = ParetoSearchLabel( node_id=target_node_id, cost_vector=new_cost_vector, previous_label_id=label_id, @@ -435,11 +425,11 @@ def is_active_label(node_id: int, label_id: int) -> bool: target_label_ids = labels_at.get(destination_node_id, []) - return labels, filter_nondominated_destination_label_ids(labels, target_label_ids) + return labels, _select_nondominated_destination_label_ids(labels, target_label_ids) -def reconstruct_label_node_path( - labels: list[ParetoPathLabel], +def _reconstruct_node_path_from_label( + labels: list[ParetoSearchLabel], label_id: int, ) -> list[int]: """Reconstruct the node path of a Pareto label.""" @@ -455,8 +445,8 @@ def reconstruct_label_node_path( return node_path -def reconstruct_label_edge_keys( - labels: list[ParetoPathLabel], +def _reconstruct_edge_key_path_from_label( + labels: list[ParetoSearchLabel], label_id: int, ) -> list[tuple[int, int, int]]: """Reconstruct the traversed edge keys of a Pareto label.""" @@ -476,10 +466,10 @@ def reconstruct_label_edge_keys( return edge_keys -def get_edge_attributes_for_edge_key( +def _get_edge_attributes_by_key( graph: MultiDiGraphAny, edge_key: tuple[int, int, int], -) -> EdgeAttributes: +) -> EdgeAttributeMap: """Load edge attributes for a specific parallel edge key.""" source_node_id, target_node_id, parallel_edge_key = edge_key edge_attributes = graph.get_edge_data( @@ -488,25 +478,25 @@ def get_edge_attributes_for_edge_key( parallel_edge_key, ) - return cast("EdgeAttributes", edge_attributes) + return cast("EdgeAttributeMap", edge_attributes) -def sum_route_cost_vectors( - segment_edge_attributes: list[EdgeAttributes], -) -> RouteCostVector: +def _sum_path_costs( + path_edge_attributes: list[EdgeAttributeMap], +) -> ParetoCostVector: """Sum objective cost components across a route.""" total_distance = 0.0 total_snow_penalty = 0.0 total_uphill_penalty = 0.0 total_scenic_penalty = 0.0 - for edge_attributes in segment_edge_attributes: + for edge_attributes in path_edge_attributes: ( distance, snow_penalty, uphill_penalty, scenic_penalty, - ) = compute_edge_objective_components(edge_attributes) + ) = compute_edge_cost_components(edge_attributes) total_distance += distance total_snow_penalty += snow_penalty total_uphill_penalty += uphill_penalty @@ -520,68 +510,68 @@ def sum_route_cost_vectors( ) -def compute_route_selection_score( - route_cost_vector: RouteCostVector, - route_objective_weights: RouteObjectiveWeights, +def _compute_route_ranking_score( + route_cost_vector: ParetoCostVector, + route_preference_weights: RoutePreferenceWeights, ) -> float: """Scalarize a route cost vector using the configured objective weights.""" - normalized_weights = normalize_route_objective_weights(route_objective_weights) + normalized_weights = normalize_route_preference_weights(route_preference_weights) distance, snow_penalty, uphill_penalty, scenic_penalty = route_cost_vector return ( distance - + normalized_weights.avoid_snow * snow_penalty - + normalized_weights.avoid_uphill * uphill_penalty - + normalized_weights.scenic * scenic_penalty + + normalized_weights.snow_free_weight * snow_penalty + + normalized_weights.flat_weight * uphill_penalty + + normalized_weights.scenic_weight * scenic_penalty ) -def build_resolved_route_candidate( - path_node_ids: list[int], - segment_edge_attributes: list[EdgeAttributes], +def _build_route_candidate( + node_path: list[int], + path_edge_attributes: list[EdgeAttributeMap], *, - route_objective_weights: RouteObjectiveWeights, + route_preference_weights: RoutePreferenceWeights, pareto_rank: int | None = None, -) -> ResolvedRouteCandidate: +) -> RouteCandidate: """Build a resolved route candidate from a path and edge sequence.""" - total_cost_vector = sum_route_cost_vectors(segment_edge_attributes) + total_cost_vector = _sum_path_costs(path_edge_attributes) - return ResolvedRouteCandidate( - path_node_ids=path_node_ids, - segment_edge_attributes=segment_edge_attributes, + return RouteCandidate( + node_path=node_path, + path_edge_attributes=path_edge_attributes, total_cost_vector=total_cost_vector, - selection_score=compute_route_selection_score( + ranking_score=_compute_route_ranking_score( total_cost_vector, - route_objective_weights, + route_preference_weights, ), pareto_rank=pareto_rank, ) -def build_single_route_candidate( - path_node_ids: list[int], +def _build_single_route_candidate( + node_path: list[int], edge_selector: EdgeSelector, *, - route_objective_weights: RouteObjectiveWeights, -) -> ResolvedRouteCandidate: + route_preference_weights: RoutePreferenceWeights, +) -> RouteCandidate: """Resolve one shortest/weighted path into a uniform route candidate.""" - segment_edge_attributes = resolve_route_segment_edges(path_node_ids, edge_selector) + segment_edge_attributes = _resolve_path_edge_attributes(node_path, edge_selector) - return build_resolved_route_candidate( - path_node_ids, + return _build_route_candidate( + node_path, segment_edge_attributes, - route_objective_weights=route_objective_weights, + route_preference_weights=route_preference_weights, ) -def build_pareto_route_candidates( +def _build_pareto_route_candidates( graph: MultiDiGraphAny, origin_node_id: int, destination_node_id: int, - route_options: RouteComputationOptions, -) -> list[ResolvedRouteCandidate]: + route_options: RoutePlanningOptions, +) -> list[RouteCandidate]: """Build sorted pareto-optimal route candidates for a source-destination pair.""" - labels, destination_label_ids = calculate_pareto_frontier_labels( + labels, destination_label_ids = run_pareto_label_search( graph, origin_node_id, destination_node_id, @@ -594,26 +584,26 @@ def build_pareto_route_candidates( ranked_label_ids = sorted( destination_label_ids, - key=lambda label_id: compute_route_selection_score( + key=lambda label_id: _compute_route_ranking_score( labels[label_id].cost_vector, - route_options.objective_weights, + route_options.preference_weights, ), )[: route_options.pareto_max_routes] - route_candidates: list[ResolvedRouteCandidate] = [] + route_candidates: list[RouteCandidate] = [] for pareto_rank, label_id in enumerate(ranked_label_ids, start=1): - path_node_ids = reconstruct_label_node_path(labels, label_id) - edge_keys = reconstruct_label_edge_keys(labels, label_id) + path_node_ids = _reconstruct_node_path_from_label(labels, label_id) + edge_keys = _reconstruct_edge_key_path_from_label(labels, label_id) segment_edge_attributes = [ - get_edge_attributes_for_edge_key(graph, edge_key) for edge_key in edge_keys + _get_edge_attributes_by_key(graph, edge_key) for edge_key in edge_keys ] route_candidates.append( - build_resolved_route_candidate( + _build_route_candidate( path_node_ids, segment_edge_attributes, - route_objective_weights=route_options.objective_weights, + route_preference_weights=route_options.preference_weights, pareto_rank=pareto_rank, ) ) @@ -621,138 +611,84 @@ def build_pareto_route_candidates( return route_candidates -def compute_route_candidates( +def _plan_route_candidates( graph: MultiDiGraphAny, origin_node_id: int, destination_node_id: int, - route_options: RouteComputationOptions, -) -> list[ResolvedRouteCandidate]: + route_options: RoutePlanningOptions, +) -> list[RouteCandidate]: """Compute resolved route candidates for the selected routing method.""" - if route_options.route_selection_method == "weighted": - path_node_ids = compute_weighted_shortest_path( + if route_options.route_optimization_method == "weighted": + path_node_ids = _compute_weighted_shortest_path( graph, origin_node_id, destination_node_id, - route_options.objective_weights, + route_options.preference_weights, ) return [ - build_single_route_candidate( + _build_single_route_candidate( path_node_ids, edge_selector=lambda source_node_id, target_node_id: ( - select_best_parallel_edge_by_scalar_cost( + select_lowest_cost_parallel_edge( graph, source_node_id, target_node_id, - normalize_route_objective_weights( - route_options.objective_weights + normalize_route_preference_weights( + route_options.preference_weights ), ) ), - route_objective_weights=route_options.objective_weights, + route_preference_weights=route_options.preference_weights, ) ] - if route_options.route_selection_method == "pareto": - return build_pareto_route_candidates( + if route_options.route_optimization_method == "pareto": + return _build_pareto_route_candidates( graph, origin_node_id, destination_node_id, route_options, ) - path_node_ids = compute_shortest_distance_path( + path_node_ids = _compute_shortest_distance_path( graph, origin_node_id, destination_node_id, ) return [ - build_single_route_candidate( + _build_single_route_candidate( path_node_ids, edge_selector=lambda source_node_id, target_node_id: ( - select_shortest_length_edge(graph, source_node_id, target_node_id) + _select_shortest_parallel_edge(graph, source_node_id, target_node_id) ), - route_objective_weights=RouteObjectiveWeights(), + route_preference_weights=RoutePreferenceWeights(), ) ] -def compute_route_path( - graph: MultiDiGraphAny, - origin_node_id: int, - destination_node_id: int, - route_options: RouteComputationOptions, -) -> list[int]: - """Compute the primary path according to the selected route method.""" - route_candidates = compute_route_candidates( - graph, - origin_node_id, - destination_node_id, - route_options, - ) - - if not route_candidates: - raise NetworkXNoPath - - return route_candidates[0].path_node_ids - - -def compute_route_steps_for_method( +def _build_route_geometry_coordinates( graph: MultiDiGraphAny, - path_node_ids: list[int], - route_options: RouteComputationOptions, -) -> list[RouteStep]: - """Build route steps for single-path routing methods.""" - if route_options.route_selection_method == "weighted": - normalized_weights = normalize_route_objective_weights( - route_options.objective_weights - ) - - return build_route_steps( - path_node_ids, - edge_selector=lambda source_node_id, target_node_id: ( - select_best_parallel_edge_by_scalar_cost( - graph, - source_node_id, - target_node_id, - normalized_weights, - ) - ), - ) - - if route_options.route_selection_method == "pareto": - return [] - - return build_route_steps( - path_node_ids, - edge_selector=lambda source_node_id, target_node_id: ( - select_shortest_length_edge(graph, source_node_id, target_node_id) - ), - ) - - -def _build_path_coordinates( - graph: MultiDiGraphAny, - path_node_ids: list[int], + node_path: list[int], ) -> list[Position2D | Position3D]: """Build route line coordinates from graph node IDs.""" coordinates: list[Position2D | Position3D] = [] - for node_id in path_node_ids: + for node_id in node_path: node_attributes = graph.nodes[node_id] - longitude = coerce_float(node_attributes.get("x"), default=0.0) - latitude = coerce_float(node_attributes.get("y"), default=0.0) + longitude = parse_float_or_default(node_attributes.get("x"), default=0.0) + latitude = parse_float_or_default(node_attributes.get("y"), default=0.0) coordinates.append(Position2D(longitude, latitude)) return coordinates -def _build_node_point(graph: MultiDiGraphAny, node_id: int) -> PydanticPoint: +def _build_node_point_feature(graph: MultiDiGraphAny, node_id: int) -> PydanticPoint: """Build a GeoJSON Point from a graph node.""" node_attributes = graph.nodes[node_id] - longitude = coerce_float(node_attributes.get("x"), default=0.0) - latitude = coerce_float(node_attributes.get("y"), default=0.0) + longitude = parse_float_or_default(node_attributes.get("x"), default=0.0) + latitude = parse_float_or_default(node_attributes.get("y"), default=0.0) return PydanticPoint( type="Point", @@ -778,15 +714,17 @@ def _find_nearest_node_id( ) -def build_route_step_responses(route_steps: list[RouteStep]) -> list[RouteStepResponse]: +def _build_route_step_summaries( + route_steps: list[AggregatedRouteStep], +) -> list[RouteStepSummary]: """Convert internal route steps into response models.""" return [ - RouteStepResponse( + RouteStepSummary( street=route_step.street, distance=route_step.distance, segment_index_from=route_step.segment_index_from, segment_index_to=route_step.segment_index_to, - objective_costs=RouteObjectiveCostBreakdown( + penalty_breakdown=RoutePenaltyBreakdown( distance=route_step.distance, snow_penalty=route_step.snow_penalty, uphill_penalty=route_step.uphill_penalty, @@ -797,13 +735,13 @@ def build_route_step_responses(route_steps: list[RouteStep]) -> list[RouteStepRe ] -def build_route_objective_cost_breakdown( - route_cost_vector: RouteCostVector, -) -> RouteObjectiveCostBreakdown: +def _build_route_penalty_breakdown( + pareto_cost_vector: ParetoCostVector, +) -> RoutePenaltyBreakdown: """Convert a route cost vector into a named response object.""" - distance, snow_penalty, uphill_penalty, scenic_penalty = route_cost_vector + distance, snow_penalty, uphill_penalty, scenic_penalty = pareto_cost_vector - return RouteObjectiveCostBreakdown( + return RoutePenaltyBreakdown( distance=distance, snow_penalty=snow_penalty, uphill_penalty=uphill_penalty, @@ -811,30 +749,34 @@ def build_route_objective_cost_breakdown( ) -def build_route_feature( +def _build_route_feature( graph: MultiDiGraphAny, - route_candidate: ResolvedRouteCandidate, + route_candidate: RouteCandidate, *, route_index: int, ) -> RouteFeature: """Serialize one resolved route candidate into a GeoJSON route feature.""" - route_steps = build_route_steps_from_edges(route_candidate.segment_edge_attributes) + route_steps = _aggregate_route_steps_from_edges( + route_candidate.path_edge_attributes + ) return RouteFeature( type="Feature", properties=RouteProperties( route_index=route_index, distance=route_candidate.total_cost_vector[0], - steps=build_route_step_responses(route_steps), - objective_costs=build_route_objective_cost_breakdown( + steps=_build_route_step_summaries(route_steps), + penalty_breakdown=_build_route_penalty_breakdown( route_candidate.total_cost_vector ), pareto_rank=route_candidate.pareto_rank, - selection_score=route_candidate.selection_score, + selection_score=route_candidate.ranking_score, ), geometry=PydanticLineString( type="LineString", - coordinates=_build_path_coordinates(graph, route_candidate.path_node_ids), + coordinates=_build_route_geometry_coordinates( + graph, route_candidate.node_path + ), ), ) @@ -843,18 +785,18 @@ def build_route_feature_collection( *, graph_state: LoadedGraphState, route_coordinates: RouteCoordinates, - transport_mode: TransportMode, - route_options: RouteComputationOptions, + travel_mode: TravelMode, + route_options: RoutePlanningOptions, ) -> RouteFeatureCollection: """Build a route FeatureCollection response for a request.""" - graph = get_graph_for_mode(graph_state, transport_mode) + graph = get_graph_for_travel_mode(graph_state, travel_mode) - validate_point_within_boundary( + validate_coordinate_within_boundary( graph_state, route_coordinates.origin_longitude, route_coordinates.origin_latitude, ) - validate_point_within_boundary( + validate_coordinate_within_boundary( graph_state, route_coordinates.destination_longitude, route_coordinates.destination_latitude, @@ -878,7 +820,7 @@ def build_route_feature_collection( ) from exception try: - route_candidates = compute_route_candidates( + route_candidates = _plan_route_candidates( graph, origin_node_id, destination_node_id, @@ -898,7 +840,7 @@ def build_route_feature_collection( return RouteFeatureCollection( type="FeatureCollection", features=[ - build_route_feature( + _build_route_feature( graph, route_candidate, route_index=route_index, @@ -906,9 +848,9 @@ def build_route_feature_collection( for route_index, route_candidate in enumerate(route_candidates) ], meta=RouteMeta( - origin=_build_node_point(graph, origin_node_id), - destination=_build_node_point(graph, destination_node_id), - route_selection_method=route_options.route_selection_method, + origin=_build_node_point_feature(graph, origin_node_id), + destination=_build_node_point_feature(graph, destination_node_id), + route_optimization_method=route_options.route_optimization_method, route_count=len(route_candidates), recommended_route_index=0, ), diff --git a/backend/app/typing_aliases.py b/backend/app/typing_aliases.py index 01d1589..98b7b02 100644 --- a/backend/app/typing_aliases.py +++ b/backend/app/typing_aliases.py @@ -10,4 +10,4 @@ MultiDiGraphAny = nx.MultiDiGraph -type EdgeAttributes = dict[str, object] +type EdgeAttributeMap = dict[str, object] diff --git a/backend/app/value_parsing.py b/backend/app/value_parsing.py index 598aece..8d62bdf 100644 --- a/backend/app/value_parsing.py +++ b/backend/app/value_parsing.py @@ -3,7 +3,7 @@ from typing import cast -def coerce_float(value: object, *, default: float = 0.0) -> float: +def parse_float_or_default(value: object, *, default: float = 0.0) -> float: """Coerce values to float with a safe default.""" try: numeric_value = cast("str | int | float", value) diff --git a/backend/data/overlays/uphill.json b/backend/data/overlays/hills.json similarity index 100% rename from backend/data/overlays/uphill.json rename to backend/data/overlays/hills.json diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 25aa0ad..36bc8c1 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cph-multi-objective-router" -version = "0.5.0" +version = "0.6.0" description = "Multi-objective urban router and navigation app" readme = "README.md" authors = [ diff --git a/backend/test.http b/backend/test.http index c584ff1..5a91080 100644 --- a/backend/test.http +++ b/backend/test.http @@ -2,69 +2,20 @@ GET http://localhost:8000/boundaries/current ### Get snow map overlay -GET http://localhost:8000/layers?overlay_attribute=snow&transport_mode=walk +GET http://localhost:8000/layers?overlay_key=snow&travel_mode=walking ### Get route by origin and destination address -POST http://localhost:8000/routes/by-address -Content-Type: application/json - -{ - "transport_mode": "bike", - "origin": "Nørrebro", - "destination": "Bispebjerg", - "route_options": { - "route_selection_method": "weighted", - "objective_weights": { - "scenic": 40, - "avoid_snow": 80, - "avoid_uphill": 20 - } - } -} +GET http://localhost:8000/routes/by-address?travel_mode=cycling&origin=Nørrebro&destination=Bispebjerg& + route_optimization_method=weighted&scenic_weight=40&snow_free_weight=80&flat_weight=20 ### Get route by origin and destination coordinates -POST http://localhost:8000/routes/by-coordinates -Content-Type: application/json - -{ - "transport_mode": "walk", - "origin": { - "type": "Point", - "coordinates": [ - 12.538235, - 55.700772 - ] - }, - "destination": { - "type": "Point", - "coordinates": [ - 12.513641, - 55.663993 - ] - }, - "route_options": { - "route_selection_method": "shortest" - } -} +GET http://localhost:8000/routes/by-coordinates?travel_mode=walking&origin_longitude=12.538235& + origin_latitude=55.700772&destination_longitude=12.513641&destination_latitude=55.663993& + route_optimization_method=shortest ### Get pareto routes by origin and destination address -POST http://localhost:8000/routes/by-address -Content-Type: application/json - -{ - "transport_mode": "bike", - "origin": "Uglevej 23", - "destination": "Fiolstræde 38", - "route_options": { - "route_selection_method": "pareto", - "objective_weights": { - "scenic": 0, - "avoid_snow": 0, - "avoid_uphill": 0 - } - } -} - +GET http://localhost:8000/routes/by-address?travel_mode=cycling&origin=Uglevej%2023&destination=Fiolstræde%2038& + route_optimization_method=pareto&scenic_weight=0&snow_free_weight=0&flat_weight=0 ### Reverse geocode coordinates to approximate address GET http://localhost:8000/geocoding/reverse?longitude=12.538235&latitude=55.700772 diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 10d88f5..f92f30e 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -48,7 +48,7 @@ def dummy_route_feature_collection() -> RouteFeatureCollection: meta=RouteMeta( origin=PydanticPoint(type="Point", coordinates=origin), destination=PydanticPoint(type="Point", coordinates=destination), - route_selection_method="shortest", + route_optimization_method="shortest", route_count=1, recommended_route_index=0, ), diff --git a/backend/tests/test_costs.py b/backend/tests/test_costs.py index 7af75cf..e0d7ea4 100644 --- a/backend/tests/test_costs.py +++ b/backend/tests/test_costs.py @@ -3,25 +3,27 @@ import networkx as nx from app import costs -from app.models import NormalizedRouteObjectiveWeights, RouteObjectiveWeights -from app.value_parsing import coerce_float +from app.models import NormalizedRoutePreferenceWeights, RoutePreferenceWeights +from app.value_parsing import parse_float_or_default def test_normalize_route_objective_weights() -> None: """Weights should be normalized from percentages to [0.0, 1.0].""" - weights = RouteObjectiveWeights(scenic=25, avoid_snow=50, avoid_uphill=75) + weights = RoutePreferenceWeights( + scenic_weight=25, snow_free_weight=50, flat_weight=75 + ) - normalized = costs.normalize_route_objective_weights(weights) + normalized = costs.normalize_route_preference_weights(weights) - assert normalized.scenic == 0.25 - assert normalized.avoid_snow == 0.5 - assert normalized.avoid_uphill == 0.75 + assert normalized.scenic_weight == 0.25 + assert normalized.snow_free_weight == 0.5 + assert normalized.flat_weight == 0.75 def test_compute_edge_objective_components() -> None: """Objective component calculation should derive penalties from edge metadata.""" - components = costs.compute_edge_objective_components( - {"length": 100.0, "snow": 0.2, "uphill": 0.3, "scenic": 0.8} + components = costs.compute_edge_cost_components( + {"length": 100.0, "snow": 0.2, "hills": 0.3, "scenic": 0.8} ) assert components[0] == 100.0 @@ -34,12 +36,12 @@ def test_networkx_weight_function_handles_direct_parallel_and_invalid_payloads() None ): """Weight function should cover direct edge dicts, parallel edges, and fallback.""" - normalized = NormalizedRouteObjectiveWeights( - scenic=0.0, - avoid_snow=1.0, - avoid_uphill=0.0, + normalized = NormalizedRoutePreferenceWeights( + scenic_weight=0.0, + snow_free_weight=1.0, + flat_weight=0.0, ) - weight_function = costs.build_networkx_weight_function(normalized) + weight_function = costs.build_weighted_edge_cost_function(normalized) direct = weight_function(1, 2, {"length": 10.0, "snow": 0.5}) parallel = weight_function( @@ -65,13 +67,13 @@ def test_select_best_parallel_edge_by_scalar_cost() -> None: _ = graph.add_edge(1, 2, length=100.0, snow=1.0) _ = graph.add_edge(1, 2, length=90.0, snow=0.0) - normalized = NormalizedRouteObjectiveWeights( - scenic=0.0, - avoid_snow=1.0, - avoid_uphill=0.0, + normalized = NormalizedRoutePreferenceWeights( + scenic_weight=0.0, + snow_free_weight=1.0, + flat_weight=0.0, ) - selected = costs.select_best_parallel_edge_by_scalar_cost(graph, 1, 2, normalized) + selected = costs.select_lowest_cost_parallel_edge(graph, 1, 2, normalized) assert selected is not None - assert coerce_float(selected["length"]) == 90.0 + assert parse_float_or_default(selected["length"]) == 90.0 diff --git a/backend/tests/test_geocoding_and_main.py b/backend/tests/test_geocoding_and_main.py index cb53cc8..c56ab24 100644 --- a/backend/tests/test_geocoding_and_main.py +++ b/backend/tests/test_geocoding_and_main.py @@ -5,26 +5,26 @@ from fastapi import HTTPException from shapely.geometry import MultiPolygon, Polygon -from app.geocoding import reverse_geocode_address +from app.geocoding import reverse_geocode_to_address from app.graph_state import GRAPH_STATE from app.main import ( - build_address_route_request_from_query, - build_coordinate_route_request_from_query, - build_pareto_routing_limits, - build_route_computation_options, - build_route_objective_weights, - create_route_from_address, - create_route_from_coordinates, + build_address_route_request_from_params, + build_coordinate_route_request_from_params, + build_pareto_search_limits, + build_route_planning_options, + build_route_preference_weights, + compute_route_by_address, + compute_route_by_coordinates, get_current_boundary, - list_layers, + list_overlay_features, reverse_geocode, ) from app.models import ( - LayerFeatureCollection, + OverlayFeatureCollection, ReverseGeocodeResponse, - RouteComputationOptions, RouteCoordinates, RouteFeatureCollection, + RoutePlanningOptions, ) @@ -57,7 +57,7 @@ def fake_get(*_args: object, **_kwargs: object) -> _FakeResponse: fake_get, ) - response = reverse_geocode_address(12.0, 55.0) + response = reverse_geocode_to_address(12.0, 55.0) assert response.address == "Test Road 5" @@ -75,7 +75,7 @@ def fake_get(*_args: object, **_kwargs: object) -> _FakeResponse: fake_get, ) - response = reverse_geocode_address(12.0, 55.0) + response = reverse_geocode_to_address(12.0, 55.0) assert response.address == "" @@ -98,14 +98,14 @@ def fake_route_builder(**_kwargs: object) -> RouteFeatureCollection: fake_route_builder, ) - request = build_address_route_request_from_query( - transport_mode="bike", + request = build_address_route_request_from_params( + travel_mode="cycling", origin="A", destination="B", - route_options=RouteComputationOptions(), + route_options=RoutePlanningOptions(), ) - result = create_route_from_address(request) + result = compute_route_by_address(request) assert result is dummy_route_feature_collection @@ -124,13 +124,13 @@ def fake_route_builder(**_kwargs: object) -> RouteFeatureCollection: fake_route_builder, ) - request = build_coordinate_route_request_from_query( - transport_mode="walk", + request = build_coordinate_route_request_from_params( + travel_mode="walking", route_coordinates=RouteCoordinates(12.0, 55.0, 12.1, 55.1), - route_options=RouteComputationOptions(), + route_options=RoutePlanningOptions(), ) - result = create_route_from_coordinates(request) + result = compute_route_by_coordinates(request) assert result is dummy_route_feature_collection @@ -142,7 +142,7 @@ def test_list_layers_and_boundary_endpoints( geodataframe = gpd.GeoDataFrame( {"geometry": []}, geometry="geometry", crs="EPSG:4326" ) - empty_collection = LayerFeatureCollection(type="FeatureCollection", features=[]) + empty_collection = OverlayFeatureCollection(type="FeatureCollection", features=[]) def fake_get_edges_for_mode( _state: object, @@ -153,23 +153,23 @@ def fake_get_edges_for_mode( def fake_build_layer_feature_collection( *_args: object, **_kwargs: object, - ) -> LayerFeatureCollection: + ) -> OverlayFeatureCollection: return empty_collection monkeypatch.setattr( - "app.main.get_edges_for_mode", + "app.main.get_edge_geodataframe_for_travel_mode", fake_get_edges_for_mode, ) monkeypatch.setattr( - "app.main.build_layer_feature_collection", + "app.main.build_overlay_feature_collection", fake_build_layer_feature_collection, ) - layers = list_layers("snow", "bike") + layers = list_overlay_features("snow", "cycling") assert layers is empty_collection - GRAPH_STATE.boundary_polygon = MultiPolygon( + GRAPH_STATE.boundary_geometry = MultiPolygon( [Polygon([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)])] ) boundary = get_current_boundary() @@ -178,7 +178,7 @@ def fake_build_layer_feature_collection( def test_get_current_boundary_raises_when_not_loaded() -> None: """Boundary endpoint should fail if startup resources were not loaded.""" - GRAPH_STATE.boundary_polygon = None + GRAPH_STATE.boundary_geometry = None with pytest.raises(HTTPException): _ = get_current_boundary() @@ -195,7 +195,7 @@ def fake_reverse_geocode_address( return ReverseGeocodeResponse(address=f"{longitude},{latitude},{zoom_level}") monkeypatch.setattr( - "app.main.reverse_geocode_address", + "app.main.reverse_geocode_to_address", fake_reverse_geocode_address, ) @@ -206,24 +206,24 @@ def fake_reverse_geocode_address( def test_build_route_computation_options_from_query_values() -> None: """Query-param option helper should construct nested routing options.""" - route_options = build_route_computation_options( - route_selection_method="pareto", - route_objective_weights=build_route_objective_weights( - scenic=10, - avoid_snow=20, - avoid_uphill=30, + route_options = build_route_planning_options( + route_optimization_method="pareto", + route_preference_weights=build_route_preference_weights( + scenic_weight=10, + snow_free_weight=20, + flat_weight=30, ), - pareto_routing_limits=build_pareto_routing_limits( + pareto_search_limits=build_pareto_search_limits( pareto_max_routes=3, pareto_max_labels_per_node=25, pareto_max_total_labels=12_000, ), ) - assert route_options.route_selection_method == "pareto" - assert route_options.objective_weights.scenic == 10 - assert route_options.objective_weights.avoid_snow == 20 - assert route_options.objective_weights.avoid_uphill == 30 + assert route_options.route_optimization_method == "pareto" + assert route_options.preference_weights.scenic_weight == 10 + assert route_options.preference_weights.snow_free_weight == 20 + assert route_options.preference_weights.flat_weight == 30 assert route_options.pareto_max_routes == 3 @@ -236,12 +236,12 @@ def failing_geocode(_value: str) -> tuple[float, float]: raise RuntimeError("failed") monkeypatch.setattr("app.main.ox.geocode", failing_geocode) - request = build_address_route_request_from_query( - transport_mode="bike", + request = build_address_route_request_from_params( + travel_mode="cycling", origin="A", destination="B", - route_options=RouteComputationOptions(), + route_options=RoutePlanningOptions(), ) with pytest.raises(HTTPException): - _ = create_route_from_address(request) + _ = compute_route_by_address(request) diff --git a/backend/tests/test_graph_state.py b/backend/tests/test_graph_state.py index 22ed6d7..3171a5d 100644 --- a/backend/tests/test_graph_state.py +++ b/backend/tests/test_graph_state.py @@ -8,11 +8,11 @@ from app.graph_state import ( LoadedGraphState, - get_edges_for_mode, - get_graph_for_mode, - load_boundary_polygon, + get_edge_geodataframe_for_travel_mode, + get_graph_for_travel_mode, + load_boundary_geometry, load_graph_state, - validate_point_within_boundary, + validate_coordinate_within_boundary, ) @@ -23,31 +23,31 @@ def test_get_graph_and_edges_for_mode() -> None: {"geometry": []}, geometry="geometry", crs="EPSG:4326" ) state = LoadedGraphState( - bike_graph=graph, - walk_graph=graph, - bike_edges=geodataframe, - walk_edges=geodataframe, + cycling_graph=graph, + walking_graph=graph, + cycling_edges=geodataframe, + walking_edges=geodataframe, ) - assert get_graph_for_mode(state, "bike") is graph - assert get_edges_for_mode(state, "walk").equals(geodataframe) + assert get_graph_for_travel_mode(state, "cycling") is graph + assert get_edge_geodataframe_for_travel_mode(state, "walking").equals(geodataframe) def test_get_graph_for_mode_raises_when_missing() -> None: """Accessor should raise HTTP 500 when graph state is missing.""" with pytest.raises(HTTPException): - _ = get_graph_for_mode(LoadedGraphState(), "bike") + _ = get_graph_for_travel_mode(LoadedGraphState(), "cycling") def test_validate_point_within_boundary() -> None: """Boundary validation should pass inside and fail outside.""" boundary = MultiPolygon([Polygon([(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0)])]) - state = LoadedGraphState(boundary_polygon=boundary) + state = LoadedGraphState(boundary_geometry=boundary) - validate_point_within_boundary(state, 1.0, 1.0) + validate_coordinate_within_boundary(state, 1.0, 1.0) with pytest.raises(HTTPException): - validate_point_within_boundary(state, 5.0, 5.0) + validate_coordinate_within_boundary(state, 5.0, 5.0) def test_load_boundary_polygon(monkeypatch: pytest.MonkeyPatch) -> None: @@ -62,7 +62,7 @@ def fake_geocode_to_gdf(_place: str) -> gpd.GeoDataFrame: monkeypatch.setattr("app.graph_state.ox.geocode_to_gdf", fake_geocode_to_gdf) - loaded_boundary = load_boundary_polygon("place") + loaded_boundary = load_boundary_geometry("place") assert loaded_boundary.geom_type == "MultiPolygon" @@ -97,13 +97,15 @@ def fake_load_boundary_polygon(_place: str) -> MultiPolygon: return boundary monkeypatch.setattr("app.graph_state.ox.graph_from_place", fake_graph_from_place) - monkeypatch.setattr("app.graph_state.apply_all_overlays", fake_apply_all_overlays) monkeypatch.setattr( - "app.graph_state.build_edge_geodataframe", + "app.graph_state.load_and_apply_overlays", fake_apply_all_overlays + ) + monkeypatch.setattr( + "app.graph_state._build_edge_geodataframe", fake_build_edge_geodataframe, ) monkeypatch.setattr( - "app.graph_state.load_boundary_polygon", + "app.graph_state.load_boundary_geometry", fake_load_boundary_polygon, ) @@ -114,11 +116,11 @@ def fake_load_boundary_polygon(_place: str) -> MultiPolygon: graph_state=state, ) - assert state.bike_graph is bike_graph - assert state.walk_graph is walk_graph - assert state.bike_edges is geodataframe - assert state.walk_edges is geodataframe - assert state.boundary_polygon is boundary + assert state.cycling_graph is bike_graph + assert state.walking_graph is walk_graph + assert state.cycling_edges is geodataframe + assert state.walking_edges is geodataframe + assert state.boundary_geometry is boundary def test_load_boundary_polygon_raises_for_invalid_geometry( @@ -136,4 +138,4 @@ def fake_geocode_to_gdf(_place: str) -> gpd.GeoDataFrame: monkeypatch.setattr("app.graph_state.ox.geocode_to_gdf", fake_geocode_to_gdf) with pytest.raises(TypeError): - _ = load_boundary_polygon("place") + _ = load_boundary_geometry("place") diff --git a/backend/tests/test_layer_service.py b/backend/tests/test_layer_service.py index 5e106dd..c7716cb 100644 --- a/backend/tests/test_layer_service.py +++ b/backend/tests/test_layer_service.py @@ -8,12 +8,12 @@ from shapely.geometry import LineString, Point from app.layer_service import ( - build_layer_feature_collection, - build_layer_features, - filter_edges_for_layer, - parse_bounding_box, + build_overlay_feature_collection, + build_overlay_features, + filter_edges_for_overlay, + parse_bounding_box_string, ) -from app.value_parsing import coerce_float +from app.value_parsing import parse_float_or_default def _build_layer_gdf() -> gpd.GeoDataFrame: @@ -35,26 +35,26 @@ def _build_layer_gdf() -> gpd.GeoDataFrame: def test_parse_bounding_box_valid_and_invalid() -> None: """Bounding box parser should accept valid input and reject malformed input.""" - assert parse_bounding_box("12.0,55.0,13.0,56.0") == (12.0, 55.0, 13.0, 56.0) + assert parse_bounding_box_string("12.0,55.0,13.0,56.0") == (12.0, 55.0, 13.0, 56.0) with pytest.raises(HTTPException): - _ = parse_bounding_box("12.0,55.0,13.0") + _ = parse_bounding_box_string("12.0,55.0,13.0") def test_filter_edges_for_layer_applies_threshold_and_limit() -> None: """Layer filtering should keep rows above threshold and respect limit.""" geodataframe = _build_layer_gdf() - filtered = filter_edges_for_layer( + filtered = filter_edges_for_overlay( geodataframe, bounding_box=None, - overlay_attribute="snow", - minimum_attribute_value=0.5, - feature_limit=10, + overlay_key="snow", + minimum_overlay_value=0.5, + max_features=10, ) assert len(filtered) == 1 - assert coerce_float(cast("object", filtered.iloc[0]["snow"])) == 0.9 + assert parse_float_or_default(cast("object", filtered.iloc[0]["snow"])) == 0.9 def test_filter_edges_for_layer_raises_for_missing_attribute() -> None: @@ -62,12 +62,12 @@ def test_filter_edges_for_layer_raises_for_missing_attribute() -> None: geodataframe = _build_layer_gdf() with pytest.raises(HTTPException): - _ = filter_edges_for_layer( + _ = filter_edges_for_overlay( geodataframe, bounding_box=None, - overlay_attribute="uphill", - minimum_attribute_value=0.0, - feature_limit=10, + overlay_key="hills", + minimum_overlay_value=0.0, + max_features=10, ) @@ -75,15 +75,15 @@ def test_build_layer_features_and_collection() -> None: """Feature builders should only include line geometries.""" geodataframe = _build_layer_gdf() - features = build_layer_features(geodataframe, "snow") - collection = build_layer_feature_collection( + features = build_overlay_features(geodataframe, "snow") + collection = build_overlay_feature_collection( geodataframe, - overlay_attribute="snow", + overlay_key="snow", bounding_box=None, - minimum_attribute_value=0.0, - feature_limit=10, + minimum_overlay_value=0.0, + max_features=10, ) assert len(features) == 1 - assert features[0].properties.overlay_attribute == "snow" + assert features[0].properties.overlay_key == "snow" assert len(collection.features) == 1 diff --git a/backend/tests/test_overlays.py b/backend/tests/test_overlays.py index 1739a0d..4a8948c 100644 --- a/backend/tests/test_overlays.py +++ b/backend/tests/test_overlays.py @@ -8,12 +8,12 @@ from shapely.geometry import MultiPolygon, Point, Polygon from app.overlays import ( - apply_overlay_attribute, - get_edge_linestring, - initialize_overlay_attributes, - load_overlay_polygons, + apply_overlay_key, + get_edge_geometry_linestring, + initialize_edge_overlay_values, + load_overlay_geometries, ) -from app.value_parsing import coerce_float +from app.value_parsing import parse_float_or_default @pytest.fixture @@ -31,7 +31,7 @@ def test_get_edge_linestring_builds_fallback_from_nodes( overlay_graph: nx.MultiDiGraph[int], ) -> None: """When geometry is missing, edge linestring should be derived from node coords.""" - edge_line = get_edge_linestring(overlay_graph, 1, 2, {"length": 2.0}) + edge_line = get_edge_geometry_linestring(overlay_graph, 1, 2, {"length": 2.0}) assert list(edge_line.coords) == [(0.0, 0.0), (2.0, 0.0)] @@ -40,12 +40,12 @@ def test_initialize_and_apply_overlay_attribute( overlay_graph: nx.MultiDiGraph[int], ) -> None: """Overlay application should set the attribute on covering edges.""" - initialize_overlay_attributes(overlay_graph) + initialize_edge_overlay_values(overlay_graph) polygon = Polygon([(0.5, -1.0), (1.5, -1.0), (1.5, 1.0), (0.5, 1.0)]) - apply_overlay_attribute( + apply_overlay_key( overlay_graph, - overlay_attribute="snow", + overlay_key="snow", overlay_polygons=[polygon], overlay_values=[0.8], ) @@ -53,7 +53,7 @@ def test_initialize_and_apply_overlay_attribute( edge_data = overlay_graph.get_edge_data(1, 2) assert edge_data is not None first_edge = edge_data[0] - assert coerce_float(cast("object", first_edge["snow"])) == 0.8 + assert parse_float_or_default(cast("object", first_edge["snow"])) == 0.8 def test_load_overlay_polygons_handles_polygon_and_multipolygon( @@ -75,7 +75,7 @@ def fake_read_file(_path: object) -> gpd.GeoDataFrame: monkeypatch.setattr("app.overlays.gpd.read_file", fake_read_file) - polygons, values = load_overlay_polygons("data/overlays/snow.json") + polygons, values = load_overlay_geometries("data/overlays/snow.json") assert len(polygons) == 2 assert values == [0.2, 0.4] @@ -99,4 +99,4 @@ def fake_read_file(_path: object) -> gpd.GeoDataFrame: monkeypatch.setattr("app.overlays.gpd.read_file", fake_read_file) with pytest.raises(TypeError): - _ = load_overlay_polygons("data/overlays/scenic.json") + _ = load_overlay_geometries("data/overlays/scenic.json") diff --git a/backend/tests/test_route_planner.py b/backend/tests/test_route_planner.py index 3bb24b7..72929ad 100644 --- a/backend/tests/test_route_planner.py +++ b/backend/tests/test_route_planner.py @@ -5,93 +5,14 @@ from fastapi import HTTPException from app.graph_state import LoadedGraphState -from app.models import RouteComputationOptions, RouteCoordinates, RouteObjectiveWeights +from app.models import RouteCoordinates, RoutePlanningOptions, RoutePreferenceWeights from app.route_planner import ( build_route_feature_collection, - build_route_steps, - calculate_pareto_frontier_labels, - compute_route_path, - compute_route_steps_for_method, dominates_cost_vector, + run_pareto_label_search, ) -def test_build_route_steps_merges_adjacent_segments() -> None: - """Step builder should merge contiguous segments on the same resolved street.""" - path = [1, 2, 3] - - def edge_selector(source: int, target: int) -> dict[str, object] | None: - if source == 1 and target == 2: - return {"name": "Main", "length": 10.0} - - if source == 2 and target == 3: - return {"name": "Main", "length": 20.0} - - return None - - steps = build_route_steps(path, edge_selector) - - assert len(steps) == 1 - assert steps[0].street == "Main" - assert steps[0].distance == 30.0 - assert steps[0].snow_penalty == 0.0 - assert steps[0].uphill_penalty == 0.0 - assert steps[0].scenic_penalty == 30.0 - - -def test_compute_route_path_for_methods(simple_graph: nx.MultiDiGraph[int]) -> None: - """Route path selection should cover shortest, weighted, and pareto modes.""" - shortest_options = RouteComputationOptions(route_selection_method="shortest") - weighted_options = RouteComputationOptions( - route_selection_method="weighted", - objective_weights=RouteObjectiveWeights(scenic=0, avoid_snow=0, avoid_uphill=0), - ) - pareto_graph: nx.MultiDiGraph[int] = nx.MultiDiGraph() - pareto_graph.add_node(1, x=12.0, y=55.0) - pareto_graph.add_node(2, x=12.1, y=55.0) - pareto_graph.add_node(3, x=12.0, y=55.1) - pareto_graph.add_node(4, x=12.1, y=55.1) - _ = pareto_graph.add_edge(1, 2, length=50.0, snow=1.0, scenic=0.0) - _ = pareto_graph.add_edge(2, 4, length=50.0, snow=1.0, scenic=0.0) - _ = pareto_graph.add_edge(1, 3, length=80.0, snow=0.0, scenic=1.0) - _ = pareto_graph.add_edge(3, 4, length=80.0, snow=0.0, scenic=1.0) - pareto_options = RouteComputationOptions( - route_selection_method="pareto", - objective_weights=RouteObjectiveWeights( - scenic=0, - avoid_snow=100, - avoid_uphill=0, - ), - ) - - shortest_path = compute_route_path(simple_graph, 1, 3, shortest_options) - weighted_path = compute_route_path(simple_graph, 1, 3, weighted_options) - pareto_path = compute_route_path(pareto_graph, 1, 4, pareto_options) - - assert shortest_path == [1, 2, 3] - assert weighted_path == [1, 2, 3] - assert pareto_path == [1, 3, 4] - - -def test_compute_route_steps_for_method(simple_graph: nx.MultiDiGraph[int]) -> None: - """Step strategy should return steps for shortest/weighted single-path methods.""" - path = [1, 2, 3] - - shortest_steps = compute_route_steps_for_method( - simple_graph, - path, - RouteComputationOptions(route_selection_method="shortest"), - ) - weighted_steps = compute_route_steps_for_method( - simple_graph, - path, - RouteComputationOptions(route_selection_method="weighted"), - ) - - assert len(shortest_steps) == 1 - assert len(weighted_steps) == 1 - - def test_dominates_cost_vector() -> None: """Pareto dominance should require no-worse costs and one strict improvement.""" assert dominates_cost_vector((1.0, 2.0, 3.0, 4.0), (1.0, 3.0, 3.0, 5.0)) @@ -110,7 +31,7 @@ def test_calculate_pareto_frontier_labels_returns_nondominated_routes() -> None: _ = graph.add_edge(1, 3, length=80.0, snow=0.0, scenic=1.0) _ = graph.add_edge(3, 4, length=80.0, snow=0.0, scenic=1.0) - _, destination_label_ids = calculate_pareto_frontier_labels( + _, destination_label_ids = run_pareto_label_search( graph, 1, 4, @@ -126,7 +47,7 @@ def test_build_route_feature_collection_success( simple_graph: nx.MultiDiGraph[int], ) -> None: """Feature collection builder should create geometry, metadata, and distance.""" - state = LoadedGraphState(bike_graph=simple_graph) + state = LoadedGraphState(cycling_graph=simple_graph) call_count = {"value": 0} @@ -151,8 +72,8 @@ def fake_nearest_nodes( feature_collection = build_route_feature_collection( graph_state=state, route_coordinates=RouteCoordinates(12.0, 55.0, 12.2, 55.2), - transport_mode="bike", - route_options=RouteComputationOptions(route_selection_method="shortest"), + travel_mode="cycling", + route_options=RoutePlanningOptions(route_optimization_method="shortest"), ) assert len(feature_collection.features) == 1 @@ -160,7 +81,7 @@ def fake_nearest_nodes( assert feature_collection.features[0].properties.distance == 220.0 assert feature_collection.meta.origin.coordinates[0] == 12.0 assert feature_collection.meta.destination.coordinates[0] == 12.2 - assert feature_collection.meta.route_selection_method == "shortest" + assert feature_collection.meta.route_optimization_method == "shortest" assert feature_collection.meta.route_count == 1 @@ -177,7 +98,7 @@ def test_build_route_feature_collection_returns_pareto_routes( _ = graph.add_edge(2, 4, length=50.0, snow=1.0, scenic=0.0, name="Snow Road") _ = graph.add_edge(1, 3, length=80.0, snow=0.0, scenic=1.0, name="Scenic Way") _ = graph.add_edge(3, 4, length=80.0, snow=0.0, scenic=1.0, name="Scenic Way") - state = LoadedGraphState(bike_graph=graph) + state = LoadedGraphState(cycling_graph=graph) sequence = [1, 4] @@ -198,33 +119,33 @@ def fake_nearest_nodes( feature_collection = build_route_feature_collection( graph_state=state, route_coordinates=RouteCoordinates(12.0, 55.0, 12.1, 55.1), - transport_mode="bike", - route_options=RouteComputationOptions( - route_selection_method="pareto", - objective_weights=RouteObjectiveWeights( - scenic=0, - avoid_snow=100, - avoid_uphill=0, + travel_mode="cycling", + route_options=RoutePlanningOptions( + route_optimization_method="pareto", + preference_weights=RoutePreferenceWeights( + scenic_weight=0, + snow_free_weight=100, + flat_weight=0, ), pareto_max_routes=2, ), ) assert len(feature_collection.features) == 2 - assert feature_collection.meta.route_selection_method == "pareto" + assert feature_collection.meta.route_optimization_method == "pareto" assert feature_collection.meta.route_count == 2 assert feature_collection.features[0].properties.route_index == 0 assert feature_collection.features[0].properties.pareto_rank == 1 assert feature_collection.features[0].properties.steps[0].street == "Scenic Way" - assert feature_collection.features[0].properties.objective_costs is not None + assert feature_collection.features[0].properties.penalty_breakdown is not None assert ( - feature_collection.features[0].properties.steps[0].objective_costs.distance + feature_collection.features[0].properties.steps[0].penalty_breakdown.distance == 160.0 ) assert ( feature_collection.features[0] .properties.steps[0] - .objective_costs.scenic_penalty + .penalty_breakdown.scenic_penalty == 0.0 ) assert feature_collection.features[1].properties.route_index == 1 @@ -236,7 +157,7 @@ def test_build_route_feature_collection_handles_snapping_error( simple_graph: nx.MultiDiGraph[int], ) -> None: """Snapping failures should be mapped to HTTP 400.""" - state = LoadedGraphState(bike_graph=simple_graph) + state = LoadedGraphState(cycling_graph=simple_graph) def fake_nearest_nodes( _graph: nx.MultiDiGraph[int], @@ -255,8 +176,8 @@ def fake_nearest_nodes( _ = build_route_feature_collection( graph_state=state, route_coordinates=RouteCoordinates(12.0, 55.0, 12.2, 55.2), - transport_mode="bike", - route_options=RouteComputationOptions(route_selection_method="shortest"), + travel_mode="cycling", + route_options=RoutePlanningOptions(route_optimization_method="shortest"), ) @@ -267,7 +188,7 @@ def test_build_route_feature_collection_handles_no_path( graph: nx.MultiDiGraph[int] = nx.MultiDiGraph() graph.add_node(1, x=12.0, y=55.0) graph.add_node(2, x=12.2, y=55.2) - state = LoadedGraphState(bike_graph=graph) + state = LoadedGraphState(cycling_graph=graph) sequence = [1, 2] @@ -288,6 +209,6 @@ def fake_nearest_nodes( _ = build_route_feature_collection( graph_state=state, route_coordinates=RouteCoordinates(12.0, 55.0, 12.2, 55.2), - transport_mode="bike", - route_options=RouteComputationOptions(route_selection_method="shortest"), + travel_mode="cycling", + route_options=RoutePlanningOptions(route_optimization_method="shortest"), ) diff --git a/backend/tests/test_value_parsing_and_models.py b/backend/tests/test_value_parsing_and_models.py index 98b7b54..902545c 100644 --- a/backend/tests/test_value_parsing_and_models.py +++ b/backend/tests/test_value_parsing_and_models.py @@ -1,22 +1,22 @@ """Tests for lightweight parsing and model behavior.""" -from app.models import RouteComputationOptions -from app.value_parsing import coerce_float +from app.models import RoutePlanningOptions +from app.value_parsing import parse_float_or_default DEFAULT_INTEGER = 0 def test_coerce_float_handles_valid_and_invalid_values() -> None: """coerce_float should parse numbers and fallback to default for invalid inputs.""" - assert coerce_float("1.5") == 1.5 - assert coerce_float(2) == 2.0 - assert coerce_float("bad", default=3.0) == 3.0 - assert coerce_float(None, default=4.0) == 4.0 + assert parse_float_or_default("1.5") == 1.5 + assert parse_float_or_default(2) == 2.0 + assert parse_float_or_default("bad", default=3.0) == 3.0 + assert parse_float_or_default(None, default=4.0) == 4.0 def test_route_option_defaults_use_shortest_method() -> None: """Default options should use the shortest-path method.""" - options = RouteComputationOptions() + options = RoutePlanningOptions() - assert options.route_selection_method == "shortest" - assert options.objective_weights.scenic == DEFAULT_INTEGER + assert options.route_optimization_method == "shortest" + assert options.preference_weights.scenic_weight == DEFAULT_INTEGER diff --git a/backend/uv.lock b/backend/uv.lock index 9d3ace2..7709bcd 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -149,14 +149,14 @@ wheels = [ [[package]] name = "basedpyright" -version = "1.38.4" +version = "1.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodejs-wheel-binaries" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/b4/26cb812eaf8ab56909c792c005fe1690706aef6f21d61107639e46e9c54c/basedpyright-1.38.4.tar.gz", hash = "sha256:8e7d4f37ffb6106621e06b9355025009cdf5b48f71c592432dd2dd304bf55e70", size = 25354730, upload-time = "2026-03-25T13:50:44.353Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/f4/4a77cc1ffb3dab7391642cde30163961d8ee973e9e6b6740c7d15aa3d3ba/basedpyright-1.39.0.tar.gz", hash = "sha256:6666f51c378c7ac45877c4c1c7041ee0b5b83d755ebc82f898f47b6fafe0cc4f", size = 25357403, upload-time = "2026-04-01T12:27:41.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/0b/3f95fd47def42479e61077523d3752086d5c12009192a7f1c9fd5507e687/basedpyright-1.38.4-py3-none-any.whl", hash = "sha256:90aa067cf3e8a3c17ad5836a72b9e1f046bc72a4ad57d928473d9368c9cd07a2", size = 12352258, upload-time = "2026-03-25T13:50:41.059Z" }, + { url = "https://files.pythonhosted.org/packages/97/47/08145d1bcc3083ed20059bdecbde404bd767f91b91e2764ec01cffec9f4b/basedpyright-1.39.0-py3-none-any.whl", hash = "sha256:91b8ad50bc85ee4a985b928f9368c35c99eee5a56c44e99b2442fa12ecc3d670", size = 12353868, upload-time = "2026-04-01T12:27:38.495Z" }, ] [[package]] @@ -233,43 +233,43 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, - { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, - { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, - { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, - { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, - { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, - { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, - { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, - { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, - { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, - { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, - { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, - { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, - { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, - { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, - { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, - { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, - { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, - { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, - { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, - { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, - { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, - { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, - { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, - { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, - { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, - { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, - { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, - { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, - { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] [[package]] @@ -388,7 +388,7 @@ wheels = [ [[package]] name = "cph-multi-objective-router" -version = "0.5.0" +version = "0.6.0" source = { virtual = "." } dependencies = [ { name = "fastapi", extra = ["standard"] }, @@ -506,7 +506,7 @@ wheels = [ [[package]] name = "fastapi" -version = "0.135.2" +version = "0.135.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -515,9 +515,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/73/5903c4b13beae98618d64eb9870c3fac4f605523dd0312ca5c80dadbd5b9/fastapi-0.135.2.tar.gz", hash = "sha256:88a832095359755527b7f63bb4c6bc9edb8329a026189eed83d6c1afcf419d56", size = 395833, upload-time = "2026-03-23T14:12:41.697Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/e6/7adb4c5fa231e82c35b8f5741a9f2d055f520c29af5546fd70d3e8e1cd2e/fastapi-0.135.3.tar.gz", hash = "sha256:bd6d7caf1a2bdd8d676843cdcd2287729572a1ef524fc4d65c17ae002a1be654", size = 396524, upload-time = "2026-04-01T16:23:58.188Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/ea/18f6d0457f9efb2fc6fa594857f92810cadb03024975726db6546b3d6fcf/fastapi-0.135.2-py3-none-any.whl", hash = "sha256:0af0447d541867e8db2a6a25c23a8c4bd80e2394ac5529bd87501bbb9e240ca5", size = 117407, upload-time = "2026-03-23T14:12:43.284Z" }, + { url = "https://files.pythonhosted.org/packages/84/a4/5caa2de7f917a04ada20018eccf60d6cc6145b0199d55ca3711b0fc08312/fastapi-0.135.3-py3-none-any.whl", hash = "sha256:9b0f590c813acd13d0ab43dd8494138eb58e484bfac405db1f3187cfc5810d98", size = 117734, upload-time = "2026-04-01T16:23:59.328Z" }, ] [package.optional-dependencies] @@ -972,14 +972,14 @@ wheels = [ [[package]] name = "jupyter-lsp" -version = "2.3.0" +version = "2.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-server" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/5a/9066c9f8e94ee517133cd98dba393459a16cd48bba71a82f16a65415206c/jupyter_lsp-2.3.0.tar.gz", hash = "sha256:458aa59339dc868fb784d73364f17dbce8836e906cd75fd471a325cba02e0245", size = 54823, upload-time = "2025-08-27T17:47:34.671Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/ff/1e4a61f5170a9a1d978f3ac3872449de6c01fc71eaf89657824c878b1549/jupyter_lsp-2.3.1.tar.gz", hash = "sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6", size = 55677, upload-time = "2026-04-02T08:10:06.749Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl", hash = "sha256:e914a3cb2addf48b1c7710914771aaf1819d46b2e5a79b0f917b5478ec93f34f", size = 76687, upload-time = "2025-08-27T17:47:33.15Z" }, + { url = "https://files.pythonhosted.org/packages/23/e8/9d61dcbd1dce8ef418f06befd4ac084b4720429c26b0b1222bc218685eff/jupyter_lsp-2.3.1-py3-none-any.whl", hash = "sha256:71b954d834e85ff3096400554f2eefaf7fe37053036f9a782b0f7c5e42dadb81", size = 77513, upload-time = "2026-04-02T08:10:01.753Z" }, ] [[package]] @@ -1465,35 +1465,35 @@ wheels = [ [[package]] name = "pillow" -version = "12.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, - { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, - { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, - { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, - { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, - { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, - { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, - { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, - { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, - { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, - { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, - { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, - { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, - { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, - { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, - { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, - { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, - { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, - { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, ] [[package]] @@ -2355,7 +2355,7 @@ wheels = [ [[package]] name = "types-geopandas" -version = "1.1.3.20260311" +version = "1.1.3.20260402" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, @@ -2363,45 +2363,45 @@ dependencies = [ { name = "pyproj" }, { name = "types-shapely" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/c5/3f9afde739c3f4eb9f6f9ac6b53af9ea9343cb0557d71b98db49b0a466f8/types_geopandas-1.1.3.20260311.tar.gz", hash = "sha256:d010583bdd1a6d6ea1f72fa5d3c5d1f9f303fb71fdb3d3789bb4f2a6a8c356c4", size = 23576, upload-time = "2026-03-11T03:58:20.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/68/5da80ff0bed00f833c1a89b3d0e4e3e4496dab4895b30dcf1c93680d3105/types_geopandas-1.1.3.20260402.tar.gz", hash = "sha256:0e3be3036eeea790e426b90f5ef540b367bb54d57823bf335d4eb70915c28087", size = 23632, upload-time = "2026-04-02T04:22:50.451Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/66/047d344515bc6f6db52aedb31f9ecb4298cb579bb49f90e55278cb55a64d/types_geopandas-1.1.3.20260311-py3-none-any.whl", hash = "sha256:d26766142f1fb4782d9e1701a489fbdbcba870b25723fbb3a02c7762c4a5d3fa", size = 31756, upload-time = "2026-03-11T03:58:19.078Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b6/f88191edabbc0539f41735a0c14a8f40b5cfef0bb7953f23537cb96a4668/types_geopandas-1.1.3.20260402-py3-none-any.whl", hash = "sha256:0641d722646eeef64edc9275549fef842d7e932457adbdd61bcd93320d7d6837", size = 31758, upload-time = "2026-04-02T04:22:49.387Z" }, ] [[package]] name = "types-networkx" -version = "3.6.1.20260321" +version = "3.6.1.20260402" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a5/0e/d40bb8d5d37bed31f4f87f7081c05bb09c56f2f1a636ca7df340a50478d6/types_networkx-3.6.1.20260321.tar.gz", hash = "sha256:8012c9f3a1ba9ad357df649f8bf633968216847379b5baa7070fced27c1599f9", size = 73812, upload-time = "2026-03-21T03:53:40.519Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/0f/5520afe8510f497a2149a0bd66400796de09e503b30d6a47a055cbae918c/types_networkx-3.6.1.20260402.tar.gz", hash = "sha256:0134981a60c6725949ddaa9fc0d3b0bbaa553f0f81e43f15e282be1b75d2db00", size = 73790, upload-time = "2026-04-02T04:20:07.538Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/8e/4c13783c3ef1ceb1a98813786976177db5d30be2e8e638bb2c5ef36bfe68/types_networkx-3.6.1.20260321-py3-none-any.whl", hash = "sha256:be98c80a347daf39a947643f89c1d8753a3788994d87deced86f1df2d0a761c0", size = 162721, upload-time = "2026-03-21T03:53:39.281Z" }, + { url = "https://files.pythonhosted.org/packages/9b/60/57fe0ebec42315a70f34920c325a803f4c069f33025043f45ac4a4266faf/types_networkx-3.6.1.20260402-py3-none-any.whl", hash = "sha256:2ea0075288632bf777e877b3c2a0d1d6991844a08a0b4a72d23c513436d64038", size = 162536, upload-time = "2026-04-02T04:20:06.375Z" }, ] [[package]] name = "types-requests" -version = "2.33.0.20260327" +version = "2.33.0.20260402" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/5f/2e3dbae6e21be6ae026563bad96cbf76602d73aa85ea09f13419ddbdabb4/types_requests-2.33.0.20260327.tar.gz", hash = "sha256:f4f74f0b44f059e3db420ff17bd1966e3587cdd34062fe38a23cda97868f8dd8", size = 23804, upload-time = "2026-03-27T04:23:38.737Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/7b/a06527d20af1441d813360b8e0ce152a75b7d8e4aab7c7d0a156f405d7ec/types_requests-2.33.0.20260402.tar.gz", hash = "sha256:1bdd3ada9b869741c5c4b887d2c8b4e38284a1449751823b5ebbccba3eefd9da", size = 23851, upload-time = "2026-04-02T04:19:55.942Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/55/951e733616c92cb96b57554746d2f65f4464d080cc2cc093605f897aba89/types_requests-2.33.0.20260327-py3-none-any.whl", hash = "sha256:fde0712be6d7c9a4d490042d6323115baf872d9a71a22900809d0432de15776e", size = 20737, upload-time = "2026-03-27T04:23:37.813Z" }, + { url = "https://files.pythonhosted.org/packages/51/65/3853bb6bac5ae789dc7e28781154705c27859eccc8e46282c3f36780f5f5/types_requests-2.33.0.20260402-py3-none-any.whl", hash = "sha256:c98372d7124dd5d10af815ee25c013897592ff92af27b27e22c98984102c3254", size = 20739, upload-time = "2026-04-02T04:19:54.955Z" }, ] [[package]] name = "types-shapely" -version = "2.1.0.20250917" +version = "2.1.0.20260402" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fa/19/7f28b10994433d43b9caa66f3b9bd6a0a9192b7ce8b5a7fc41534e54b821/types_shapely-2.1.0.20250917.tar.gz", hash = "sha256:5c56670742105aebe40c16414390d35fcaa55d6f774d328c1a18273ab0e2134a", size = 26363, upload-time = "2025-09-17T02:47:44.604Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/f7/46e95b09434105d7b772d05657495f2900bae8e108fdf4e6d8b5902aa28c/types_shapely-2.1.0.20260402.tar.gz", hash = "sha256:0eb592328170433b4724430a64c309bf07ba69d5d11489d3dba21382d78f5297", size = 26481, upload-time = "2026-04-02T04:20:03.104Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/a9/554ac40810e530263b6163b30a2b623bc16aae3fb64416f5d2b3657d0729/types_shapely-2.1.0.20250917-py3-none-any.whl", hash = "sha256:9334a79339504d39b040426be4938d422cec419168414dc74972aa746a8bf3a1", size = 37813, upload-time = "2025-09-17T02:47:43.788Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/1aa3a62f5b85d4a9e649e7b42842a9e5503fef7eb50c480137a6b94f8bb1/types_shapely-2.1.0.20260402-py3-none-any.whl", hash = "sha256:8d70a16f615a104fd8abdd73e684d4e83b9dedf31d6432ecf86945b5ef0e35de", size = 37817, upload-time = "2026-04-02T04:20:02.17Z" }, ] [[package]] diff --git a/frontend/package.json b/frontend/package.json index d8bc1d0..6b24575 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "cph-multi-objective-router", - "version": "0.5.0", + "version": "0.6.0", "private": true, "type": "module", "scripts": { @@ -18,9 +18,9 @@ "@mantine/form": "^9.0.0", "@mantine/hooks": "^9.0.0", "@tabler/icons-react": "^3.41.1", - "@tanstack/query-async-storage-persister": "^5.96.0", - "@tanstack/react-query": "^5.96.0", - "@tanstack/react-query-persist-client": "^5.96.0", + "@tanstack/query-async-storage-persister": "^5.96.1", + "@tanstack/react-query": "^5.96.1", + "@tanstack/react-query-persist-client": "^5.96.1", "@turf/boolean-point-in-polygon": "^7.3.4", "@turf/helpers": "^7.3.4", "axios": "^1.14.0", @@ -35,8 +35,8 @@ "@eslint/js": "^9.39.4", "@hey-api/openapi-ts": "0.94.5", "@rolldown/plugin-babel": "^0.2.2", - "@tanstack/eslint-plugin-query": "^5.96.0", - "@tanstack/react-query-devtools": "^5.96.0", + "@tanstack/eslint-plugin-query": "^5.96.1", + "@tanstack/react-query-devtools": "^5.96.1", "@types/babel__core": "^7.20.5", "@types/geojson": "^7946.0.16", "@types/leaflet": "^1.9.21", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index af0c750..6674ee5 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -21,14 +21,14 @@ importers: specifier: ^3.41.1 version: 3.41.1(react@19.2.4) '@tanstack/query-async-storage-persister': - specifier: ^5.96.0 - version: 5.96.0 + specifier: ^5.96.1 + version: 5.96.1 '@tanstack/react-query': - specifier: ^5.96.0 - version: 5.96.0(react@19.2.4) + specifier: ^5.96.1 + version: 5.96.1(react@19.2.4) '@tanstack/react-query-persist-client': - specifier: ^5.96.0 - version: 5.96.0(@tanstack/react-query@5.96.0(react@19.2.4))(react@19.2.4) + specifier: ^5.96.1 + version: 5.96.1(@tanstack/react-query@5.96.1(react@19.2.4))(react@19.2.4) '@turf/boolean-point-in-polygon': specifier: ^7.3.4 version: 7.3.4 @@ -67,11 +67,11 @@ importers: specifier: ^0.2.2 version: 0.2.2(@babel/core@7.29.0)(@babel/runtime@7.29.2)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1))(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.8))) '@tanstack/eslint-plugin-query': - specifier: ^5.96.0 - version: 5.96.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + specifier: ^5.96.1 + version: 5.96.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) '@tanstack/react-query-devtools': - specifier: ^5.96.0 - version: 5.96.0(@tanstack/react-query@5.96.0(react@19.2.4))(react@19.2.4) + specifier: ^5.96.1 + version: 5.96.1(@tanstack/react-query@5.96.1(react@19.2.4))(react@19.2.4) '@types/babel__core': specifier: ^7.20.5 version: 7.20.5 @@ -678,8 +678,8 @@ packages: '@tabler/icons@3.41.1': resolution: {integrity: sha512-OaRnVbRmH2nHtFeg+RmMJ/7m2oBIF9XCJAUD5gQnMrpK9f05ydj8MZrAf3NZQqOXyxGN1UBL0D5IKLLEUfr74Q==} - '@tanstack/eslint-plugin-query@5.96.0': - resolution: {integrity: sha512-iPxSM1lNBzz63scYaudGeJk4yqb51MpvnX2GUmBjEvoLLwcaCTZg+OQmjmaoe5o3TApQZK8INYnmYhN4p4uxuQ==} + '@tanstack/eslint-plugin-query@5.96.1': + resolution: {integrity: sha512-BDJU+Q+zESjarSSFmbzpCBh+1wDxwW+DyQlvwIukF24MHYOoRPH4ouJRTlDdbp3BnIkeylZaHHSgIvxY9lgI/g==} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ^5.4.0 @@ -687,32 +687,32 @@ packages: typescript: optional: true - '@tanstack/query-async-storage-persister@5.96.0': - resolution: {integrity: sha512-Xlzt5UFyAkSDkZ7DwlgpPU6AHMRwn6RKp+fbyuburrPhy94/oHbX+X4jNoq+fdPzSYC8U1Y6yOfpkvRAov7ZaQ==} + '@tanstack/query-async-storage-persister@5.96.1': + resolution: {integrity: sha512-5+Log708EqBHe6bPIZvNkJvbLQ4vK7AOCOT303lWytst48ylqlkDpzMWYeR+zZLaKCsyt7X6qx5VhMWH6IOy9w==} - '@tanstack/query-core@5.96.0': - resolution: {integrity: sha512-sfO3uQeol1BU7cRP6NYY7nAiX3GiNY20lI/dtSbKLwcIkYw/X+w/tEsQAkc544AfIhBX/IvH/QYtPHrPhyAKGw==} + '@tanstack/query-core@5.96.1': + resolution: {integrity: sha512-u1yBgtavSy+N8wgtW3PiER6UpxcplMje65yXnnVgiHTqiMwLlxiw4WvQDrXyn+UD6lnn8kHaxmerJUzQcV/MMg==} - '@tanstack/query-devtools@5.96.0': - resolution: {integrity: sha512-MEdO1M/9ItB62OtTqVo8AIj/G6vJemA642N56bw8aIqpXKIj5VG/3xWgh2piw76NmoCIlapxjjWp1MMLmrvKJw==} + '@tanstack/query-devtools@5.96.1': + resolution: {integrity: sha512-A4+uQTWbiqZDgrLeyjpFYLfMaWaKWpkwTkR1cUfocVj6vPYgym7QTG2se9A01WSxceDdmgxOqvn1ivcTvgWD8w==} - '@tanstack/query-persist-client-core@5.96.0': - resolution: {integrity: sha512-iSqqhWtQ7sZqucmOIbC+eU+npaP36ZARtyjEfaDL/9A8f8P70NHe3RJ3N7fkxbkc36pQd+duANo5TTnq8yCZSg==} + '@tanstack/query-persist-client-core@5.96.1': + resolution: {integrity: sha512-61jJdGjhBaAJiQ6TRt4uFI8EoAPyf7bJjbtc59AxZwMyid9kV9FlUEtNs2TkEzYHPTYvDZzz0qOvvkn1YD/VEg==} - '@tanstack/react-query-devtools@5.96.0': - resolution: {integrity: sha512-P0WFX0s3iYii4oZTSCK9T3/PBQ9uY/SVTzcZFbyzCo5ujeIAsqos3HjBWoF/lhJXWVe8UXkjAmgXr3TUD11q2A==} + '@tanstack/react-query-devtools@5.96.1': + resolution: {integrity: sha512-3ZZ58fupIXtJFM0evj8YvWrauaZPUrQEqRYaq9e4ER/WPqTKeWEucqWCXn+KJLgWlcot5JIIUtQNynbovGjTTA==} peerDependencies: - '@tanstack/react-query': ^5.96.0 + '@tanstack/react-query': ^5.96.1 react: ^18 || ^19 - '@tanstack/react-query-persist-client@5.96.0': - resolution: {integrity: sha512-lXOIHU2i+GAG7Gm3OEMmw3xmD51H/8i99tIFaBcGW4mF0Wq91q3Q78xf5q7Cu0NI8WRcbxi7Dn6h1Fs9zMNw0A==} + '@tanstack/react-query-persist-client@5.96.1': + resolution: {integrity: sha512-HkYAI7T2uc12gvVR8P1D7dHgA/AdGDhqDN1HeaNP561OFa0H425qk61ykWtpF8Vnv82ElX6ao0nuNr1oPh89Uw==} peerDependencies: - '@tanstack/react-query': ^5.96.0 + '@tanstack/react-query': ^5.96.1 react: ^18 || ^19 - '@tanstack/react-query@5.96.0': - resolution: {integrity: sha512-6qbjdm1K5kizVKv9TNqhIN3doq2anRhdF2XaFMFSn4m8L22S69RV+FilvlyVT4RoJyMxtPU5rs4RpdFa/PEC7A==} + '@tanstack/react-query@5.96.1': + resolution: {integrity: sha512-2X7KYK5KKWUKGeWCVcqxXAkYefJtrKB7tSKWgeG++b0H6BRHxQaLSSi8AxcgjmUnnosHuh9WsFZqvE16P1WCzA==} peerDependencies: react: ^18 || ^19 @@ -996,8 +996,8 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - baseline-browser-mapping@2.10.12: - resolution: {integrity: sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==} + baseline-browser-mapping@2.10.13: + resolution: {integrity: sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==} engines: {node: '>=6.0.0'} hasBin: true @@ -1045,8 +1045,8 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - caniuse-lite@1.0.30001782: - resolution: {integrity: sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw==} + caniuse-lite@1.0.30001784: + resolution: {integrity: sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -1059,8 +1059,8 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} - citty@0.2.1: - resolution: {integrity: sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==} + citty@0.2.2: + resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} @@ -1204,8 +1204,8 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - defu@6.1.4: - resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + defu@6.1.6: + resolution: {integrity: sha512-f8mefEW4WIVg4LckePx3mALjQSPQgFlg9U8yaPdlsbdYcHQyj9n2zL2LJEA52smeYxOvmd/nB7TpMtHGMTHcug==} delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} @@ -1225,16 +1225,16 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} - dotenv@17.3.1: - resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} + dotenv@17.4.0: + resolution: {integrity: sha512-kCKF62fwtzwYm0IGBNjRUjtJgMfGapII+FslMHIjMR5KTnwEmBmWLDRSnc3XSNP8bNy34tekgQyDT0hr7pERRQ==} engines: {node: '>=12'} dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - electron-to-chromium@1.5.329: - resolution: {integrity: sha512-/4t+AS1l4S3ZC0Ja7PHFIWeBIxGA3QGqV8/yKsP36v7NcyUCl+bIcmw6s5zVuMIECWwBrAK/6QLzTmbJChBboQ==} + electron-to-chromium@1.5.331: + resolution: {integrity: sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} @@ -1872,8 +1872,8 @@ packages: node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} - node-releases@2.0.36: - resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + node-releases@2.0.37: + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} nypm@0.6.5: resolution: {integrity: sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==} @@ -2961,7 +2961,7 @@ snapshots: '@tabler/icons@3.41.1': {} - '@tanstack/eslint-plugin-query@5.96.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + '@tanstack/eslint-plugin-query@5.96.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.4(jiti@2.6.1) @@ -2970,34 +2970,34 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/query-async-storage-persister@5.96.0': + '@tanstack/query-async-storage-persister@5.96.1': dependencies: - '@tanstack/query-core': 5.96.0 - '@tanstack/query-persist-client-core': 5.96.0 + '@tanstack/query-core': 5.96.1 + '@tanstack/query-persist-client-core': 5.96.1 - '@tanstack/query-core@5.96.0': {} + '@tanstack/query-core@5.96.1': {} - '@tanstack/query-devtools@5.96.0': {} + '@tanstack/query-devtools@5.96.1': {} - '@tanstack/query-persist-client-core@5.96.0': + '@tanstack/query-persist-client-core@5.96.1': dependencies: - '@tanstack/query-core': 5.96.0 + '@tanstack/query-core': 5.96.1 - '@tanstack/react-query-devtools@5.96.0(@tanstack/react-query@5.96.0(react@19.2.4))(react@19.2.4)': + '@tanstack/react-query-devtools@5.96.1(@tanstack/react-query@5.96.1(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/query-devtools': 5.96.0 - '@tanstack/react-query': 5.96.0(react@19.2.4) + '@tanstack/query-devtools': 5.96.1 + '@tanstack/react-query': 5.96.1(react@19.2.4) react: 19.2.4 - '@tanstack/react-query-persist-client@5.96.0(@tanstack/react-query@5.96.0(react@19.2.4))(react@19.2.4)': + '@tanstack/react-query-persist-client@5.96.1(@tanstack/react-query@5.96.1(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/query-persist-client-core': 5.96.0 - '@tanstack/react-query': 5.96.0(react@19.2.4) + '@tanstack/query-persist-client-core': 5.96.1 + '@tanstack/react-query': 5.96.1(react@19.2.4) react: 19.2.4 - '@tanstack/react-query@5.96.0(react@19.2.4)': + '@tanstack/react-query@5.96.1(react@19.2.4)': dependencies: - '@tanstack/query-core': 5.96.0 + '@tanstack/query-core': 5.96.1 react: 19.2.4 '@turf/boolean-point-in-polygon@7.3.4': @@ -3360,7 +3360,7 @@ snapshots: balanced-match@4.0.4: {} - baseline-browser-mapping@2.10.12: {} + baseline-browser-mapping@2.10.13: {} brace-expansion@1.1.13: dependencies: @@ -3373,10 +3373,10 @@ snapshots: browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.12 - caniuse-lite: 1.0.30001782 - electron-to-chromium: 1.5.329 - node-releases: 2.0.36 + baseline-browser-mapping: 2.10.13 + caniuse-lite: 1.0.30001784 + electron-to-chromium: 1.5.331 + node-releases: 2.0.37 update-browserslist-db: 1.2.3(browserslist@4.28.2) bundle-name@4.1.0: @@ -3387,8 +3387,8 @@ snapshots: dependencies: chokidar: 5.0.0 confbox: 0.2.4 - defu: 6.1.4 - dotenv: 17.3.1 + defu: 6.1.6 + dotenv: 17.4.0 exsolve: 1.0.8 giget: 2.0.0 jiti: 2.6.1 @@ -3419,7 +3419,7 @@ snapshots: camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001782: {} + caniuse-lite@1.0.30001784: {} chalk@4.1.2: dependencies: @@ -3434,7 +3434,7 @@ snapshots: dependencies: consola: 3.4.2 - citty@0.2.1: {} + citty@0.2.2: {} clsx@2.1.1: {} @@ -3557,7 +3557,7 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - defu@6.1.4: {} + defu@6.1.6: {} delayed-stream@1.0.0: {} @@ -3571,7 +3571,7 @@ snapshots: dependencies: esutils: 2.0.3 - dotenv@17.3.1: {} + dotenv@17.4.0: {} dunder-proto@1.0.1: dependencies: @@ -3579,7 +3579,7 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - electron-to-chromium@1.5.329: {} + electron-to-chromium@1.5.331: {} emoji-regex@9.2.2: {} @@ -3969,7 +3969,7 @@ snapshots: dependencies: citty: 0.1.6 consola: 3.4.2 - defu: 6.1.4 + defu: 6.1.6 node-fetch-native: 1.6.7 nypm: 0.6.5 pathe: 2.0.3 @@ -4313,11 +4313,11 @@ snapshots: node-fetch-native@1.6.7: {} - node-releases@2.0.36: {} + node-releases@2.0.37: {} nypm@0.6.5: dependencies: - citty: 0.2.1 + citty: 0.2.2 pathe: 2.0.3 tinyexec: 1.0.4 @@ -4478,7 +4478,7 @@ snapshots: rc9@2.1.2: dependencies: - defu: 6.1.4 + defu: 6.1.6 destr: 2.0.5 react-dom@19.2.4(react@19.2.4): diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e8003be..818c25f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,8 +1,4 @@ -import { - type PickMode, - RoutePanel, - type RoutePanelHandle, -} from "@/RoutePanel.tsx"; +import { RoutePanel, type RoutePanelHandle } from "@/RoutePanel.tsx"; import { useEffect, useRef, useState } from "react"; import { Map } from "@/Map.tsx"; import type { @@ -11,7 +7,7 @@ import type { Point, BoundaryFeatureCollection, } from "@/client"; -import { StatusBar } from "@/StatusBar.tsx"; +import { StatusNotice } from "@/StatusNotice.tsx"; import { Box, Button, @@ -26,53 +22,59 @@ import booleanPointInPolygon from "@turf/boolean-point-in-polygon"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { isAxiosError } from "axios"; import { - createRouteFromAddressRoutesByAddressGetQueryKey, - createRouteFromAddressRoutesByAddressGetOptions, - createRouteFromCoordinatesRoutesByCoordinatesGetQueryKey, - createRouteFromCoordinatesRoutesByCoordinatesGetOptions, + computeRouteByAddressRoutesByAddressGetQueryKey, + computeRouteByAddressRoutesByAddressGetOptions, + computeRouteByCoordinatesRoutesByCoordinatesGetQueryKey, + computeRouteByCoordinatesRoutesByCoordinatesGetOptions, getCurrentBoundaryBoundariesCurrentGetOptions, reverseGeocodeGeocodingReverseGetOptions, } from "@/client/@tanstack/react-query.gen.ts"; -import type { TransportMode } from "@/types/global.ts"; +import type { + ActiveRouteEndpoint, + RouteEndpoint, + TravelMode, +} from "@/types/global.ts"; import { toTurfFeature } from "@/utils.ts"; -type MarkerKind = "origin" | "destination"; type MarkerSource = "drag" | "pick"; -export type Mode = "shortest" | "advanced"; -export type RouteSelectionMethod = "shortest" | "weighted" | "pareto"; +export type RoutePlanningMode = "shortest" | "multi-objective"; +export type RouteOptimizationMethod = "shortest" | "weighted" | "pareto"; -interface SearchSettings { - transportMode: TransportMode; - mode: Mode; - method: RouteSelectionMethod; - scenic: number; - snow: number; - uphill: number; +interface RouteSearchOptions { + travelMode: TravelMode; + routePlanningMode: RoutePlanningMode; + routeOptimizationMethod: RouteOptimizationMethod; + scenicWeight: number; + snowFreeWeight: number; + flatWeight: number; } -type AddressSearchRequest = SearchSettings & { +interface AddressRouteSearchRequest extends RouteSearchOptions { source: "address"; origin: string; destination: string; -}; -type CoordinateSearchRequest = SearchSettings & { - source: "coords"; +} + +interface CoordinateRouteSearchRequest extends RouteSearchOptions { + source: "coordinates"; origin: Point; destination: Point; -}; +} -type SearchRequest = AddressSearchRequest | CoordinateSearchRequest; +type RouteSearchRequest = + | AddressRouteSearchRequest + | CoordinateRouteSearchRequest; export interface SearchHistoryEntry { key: string; - request: SearchRequest; + request: RouteSearchRequest; originLabel: string; destinationLabel: string; createdAt: string; } -const routeStaleTimeMs = 1000 * 60 * 60 * 24; +const routeStaleTimeMs = 1000 * 60 * 60 * 24; // 24 hours const historyStorageKey = "cph-multi-objective-router:search-history:v1"; const maxHistoryEntries = 20; const sliderSearchDelayMs = 1000; @@ -87,80 +89,88 @@ const normalizePoint = (point: Point): Point => ({ ], }); -const resolveMethodForMode = ( - mode: Mode, - method: RouteSelectionMethod, -): RouteSelectionMethod => { - if (mode === "shortest") { +const resolveRouteOptimizationMethod = ( + routePlanningMode: RoutePlanningMode, + routeOptimizationMethod: RouteOptimizationMethod, +): RouteOptimizationMethod => { + if (routePlanningMode === "shortest") { return "shortest"; } - return method === "shortest" ? "weighted" : method; + return routeOptimizationMethod === "shortest" + ? "weighted" + : routeOptimizationMethod; }; -const canonicalizeSearchRequest = (request: SearchRequest): SearchRequest => { - const nextMethod = resolveMethodForMode(request.mode, request.method); +const normalizeRouteSearchRequest = ( + routeSearchRequest: RouteSearchRequest, +): RouteSearchRequest => { + const nextMethod = resolveRouteOptimizationMethod( + routeSearchRequest.routePlanningMode, + routeSearchRequest.routeOptimizationMethod, + ); + const nextWeights = - request.mode === "advanced" + routeSearchRequest.routePlanningMode === "multi-objective" ? { - scenic: request.scenic, - snow: request.snow, - uphill: request.uphill, + scenicWeight: routeSearchRequest.scenicWeight, + snowFreeWeight: routeSearchRequest.snowFreeWeight, + flatWeight: routeSearchRequest.flatWeight, } : { - scenic: 0, - snow: 0, - uphill: 0, + scenicWeight: 0, + snowFreeWeight: 0, + flatWeight: 0, }; - if (request.source === "address") { + if (routeSearchRequest.source === "address") { return { - ...request, - origin: normalizeAddress(request.origin), - destination: normalizeAddress(request.destination), - method: nextMethod, + ...routeSearchRequest, + origin: normalizeAddress(routeSearchRequest.origin), + destination: normalizeAddress(routeSearchRequest.destination), + routeOptimizationMethod: nextMethod, ...nextWeights, }; } return { - ...request, - origin: normalizePoint(request.origin), - destination: normalizePoint(request.destination), - method: nextMethod, + ...routeSearchRequest, + origin: normalizePoint(routeSearchRequest.origin), + destination: normalizePoint(routeSearchRequest.destination), + routeOptimizationMethod: nextMethod, ...nextWeights, }; }; -const buildRouteQueryKey = (request: SearchRequest) => { - const canonical = canonicalizeSearchRequest(request); +const buildRouteQueryKey = (routeSearchRequest: RouteSearchRequest) => { + const normalizedRequest = normalizeRouteSearchRequest(routeSearchRequest); - if (canonical.source === "address") { - return createRouteFromAddressRoutesByAddressGetQueryKey({ + if (normalizedRequest.source === "address") { + return computeRouteByAddressRoutesByAddressGetQueryKey({ query: { - transport_mode: canonical.transportMode, - origin: canonical.origin, - destination: canonical.destination, - route_selection_method: canonical.method, - scenic: canonical.scenic, - avoid_snow: canonical.snow, - avoid_uphill: canonical.uphill, + travel_mode: normalizedRequest.travelMode, + origin: normalizedRequest.origin, + destination: normalizedRequest.destination, + route_optimization_method: normalizedRequest.routeOptimizationMethod, + scenic_weight: normalizedRequest.scenicWeight, + snow_free_weight: normalizedRequest.snowFreeWeight, + flat_weight: normalizedRequest.flatWeight, pareto_max_routes: 3, }, }); } - return createRouteFromCoordinatesRoutesByCoordinatesGetQueryKey({ + return computeRouteByCoordinatesRoutesByCoordinatesGetQueryKey({ query: { - transport_mode: canonical.transportMode, - origin_longitude: canonical.origin.coordinates[0], - origin_latitude: canonical.origin.coordinates[1], - destination_longitude: canonical.destination.coordinates[0], - destination_latitude: canonical.destination.coordinates[1], - route_selection_method: canonical.method, - scenic: canonical.scenic, - avoid_snow: canonical.snow, - avoid_uphill: canonical.uphill, + travel_mode: normalizedRequest.travelMode, + origin_longitude: normalizedRequest.origin.coordinates[0], + origin_latitude: normalizedRequest.origin.coordinates[1], + destination_longitude: normalizedRequest.destination.coordinates[0], + destination_latitude: normalizedRequest.destination.coordinates[1], + route_optimization_method: normalizedRequest.routeOptimizationMethod, + scenic_weight: normalizedRequest.scenicWeight, + snow_free_weight: normalizedRequest.snowFreeWeight, + flat_weight: normalizedRequest.flatWeight, pareto_max_routes: 3, }, }); @@ -180,7 +190,7 @@ const upsertHistoryEntry = ( return [nextEntry, ...withoutDuplicate].slice(0, maxHistoryEntries); }; -const inBoundsBbox = ( +const isPointInsideBounds = ( point: Point, boundaryFeatureCollection: BoundaryFeatureCollection, ) => { @@ -195,7 +205,7 @@ const isInsideBoundary = ( point: Point, boundaryFeatureCollection: BoundaryFeatureCollection, ) => { - if (!inBoundsBbox(point, boundaryFeatureCollection)) { + if (!isPointInsideBounds(point, boundaryFeatureCollection)) { return false; } @@ -240,7 +250,8 @@ const App = () => { null, ); - const [pickMode, setPickMode] = useState(null); + const [activeRouteEndpoint, setActiveRouteEndpoint] = + useState(null); const [originPosition, setOriginPosition] = useState(null); const [destinationPosition, setDestinationPosition] = useState( null, @@ -252,17 +263,18 @@ const App = () => { const [statusColor, setStatusColor] = useState("green"); const statusTimeoutRef = useRef(null); - const [transportMode, setTransportMode] = useState("walk"); - const [mode, setMode] = useState("shortest"); - const [method, setMethod] = useState("shortest"); - const [scenic, setScenic] = useState(0); - const [snow, setSnow] = useState(0); - const [uphill, setUphill] = useState(0); + const [travelMode, setTravelMode] = useState("walking"); + const [routePlanningMode, setRoutePlanningMode] = + useState("shortest"); + const [routeOptimizationMethod, setRouteOptimizationMethod] = + useState("shortest"); + const [scenicWeight, setScenicWeight] = useState(0); + const [snowFreeWeight, setSnowFreeWeight] = useState(0); + const [flatWeight, setFlatWeight] = useState(0); const [routeLoading, setRouteLoading] = useState(false); - const [committedSearch, setCommittedSearch] = useState( - null, - ); + const [committedSearch, setCommittedSearch] = + useState(null); const [searchHistory, setSearchHistory, removeSearchHistory] = useLocalStorage({ key: historyStorageKey, @@ -320,98 +332,111 @@ const App = () => { ? getErrorMessage(boundaryQuery.error) : null; - const getCurrentSearchSettings = ( - overrides: Partial = {}, - ): SearchSettings => { - const nextMode = overrides.mode ?? mode; - const requestedMethod = overrides.method ?? method; + const getCurrentRouteSearchOptions = ( + overrides: Partial = {}, + ): RouteSearchOptions => { + const nextRoutePlanningMode = + overrides.routePlanningMode ?? routePlanningMode; + const requestedRouteOptimizationMethod = + overrides.routeOptimizationMethod ?? routeOptimizationMethod; return { - transportMode: overrides.transportMode ?? transportMode, - mode: nextMode, - method: resolveMethodForMode(nextMode, requestedMethod), - scenic: overrides.scenic ?? scenic, - snow: overrides.snow ?? snow, - uphill: overrides.uphill ?? uphill, + travelMode: overrides.travelMode ?? travelMode, + routePlanningMode: nextRoutePlanningMode, + routeOptimizationMethod: resolveRouteOptimizationMethod( + nextRoutePlanningMode, + requestedRouteOptimizationMethod, + ), + scenicWeight: overrides.scenicWeight ?? scenicWeight, + snowFreeWeight: overrides.snowFreeWeight ?? snowFreeWeight, + flatWeight: overrides.flatWeight ?? flatWeight, }; }; - const buildAddressSearchRequest = ( + const createAddressRouteSearchRequest = ( origin: string, destination: string, - overrides: Partial = {}, - ): AddressSearchRequest => - canonicalizeSearchRequest({ + overrides: Partial = {}, + ): AddressRouteSearchRequest => + normalizeRouteSearchRequest({ source: "address", origin, destination, - ...getCurrentSearchSettings(overrides), - }) as AddressSearchRequest; + ...getCurrentRouteSearchOptions(overrides), + }) as AddressRouteSearchRequest; - const buildCoordinateSearchRequest = ( + const createCoordinateRouteSearchRequest = ( origin: Point, destination: Point, - overrides: Partial = {}, - ): CoordinateSearchRequest => - canonicalizeSearchRequest({ - source: "coords", + overrides: Partial = {}, + ): CoordinateRouteSearchRequest => + normalizeRouteSearchRequest({ + source: "coordinates", origin, destination, - ...getCurrentSearchSettings(overrides), - }) as CoordinateSearchRequest; + ...getCurrentRouteSearchOptions(overrides), + }) as CoordinateRouteSearchRequest; - const rebuildCommittedSearch = ( - baseRequest: SearchRequest, - overrides: Partial = {}, - ): SearchRequest => { + const rebuildRouteSearchRequest = ( + baseRequest: RouteSearchRequest, + overrides: Partial = {}, + ): RouteSearchRequest => { if (baseRequest.source === "address") { - return buildAddressSearchRequest( + return createAddressRouteSearchRequest( baseRequest.origin, baseRequest.destination, overrides, ); } - return buildCoordinateSearchRequest( + return createCoordinateRouteSearchRequest( baseRequest.origin, baseRequest.destination, overrides, ); }; - const getAddressRouteQueryOptions = (request: AddressSearchRequest) => - createRouteFromAddressRoutesByAddressGetOptions({ + const getAddressRouteQueryOptions = ( + addressRouteSearchRequest: AddressRouteSearchRequest, + ) => + computeRouteByAddressRoutesByAddressGetOptions({ query: { - transport_mode: request.transportMode, - origin: request.origin, - destination: request.destination, - route_selection_method: request.method, - scenic: request.scenic, - avoid_snow: request.snow, - avoid_uphill: request.uphill, + travel_mode: addressRouteSearchRequest.travelMode, + origin: addressRouteSearchRequest.origin, + destination: addressRouteSearchRequest.destination, + route_optimization_method: + addressRouteSearchRequest.routeOptimizationMethod, + scenic_weight: addressRouteSearchRequest.scenicWeight, + snow_free_weight: addressRouteSearchRequest.snowFreeWeight, + flat_weight: addressRouteSearchRequest.flatWeight, pareto_max_routes: 3, }, }); - const getCoordinateRouteQueryOptions = (request: CoordinateSearchRequest) => - createRouteFromCoordinatesRoutesByCoordinatesGetOptions({ + const getCoordinateRouteQueryOptions = ( + coordinateRouteSearchRequest: CoordinateRouteSearchRequest, + ) => + computeRouteByCoordinatesRoutesByCoordinatesGetOptions({ query: { - transport_mode: request.transportMode, - origin_longitude: request.origin.coordinates[0], - origin_latitude: request.origin.coordinates[1], - destination_longitude: request.destination.coordinates[0], - destination_latitude: request.destination.coordinates[1], - route_selection_method: request.method, - scenic: request.scenic, - avoid_snow: request.snow, - avoid_uphill: request.uphill, + travel_mode: coordinateRouteSearchRequest.travelMode, + origin_longitude: coordinateRouteSearchRequest.origin.coordinates[0], + origin_latitude: coordinateRouteSearchRequest.origin.coordinates[1], + destination_longitude: + coordinateRouteSearchRequest.destination.coordinates[0], + destination_latitude: + coordinateRouteSearchRequest.destination.coordinates[1], + route_optimization_method: + coordinateRouteSearchRequest.routeOptimizationMethod, + scenic_weight: coordinateRouteSearchRequest.scenicWeight, + snow_free_weight: coordinateRouteSearchRequest.snowFreeWeight, + flat_weight: coordinateRouteSearchRequest.flatWeight, pareto_max_routes: 3, }, }); - const fetchRoute = async (request: SearchRequest) => { - if (request.source === "address") { - const options = getAddressRouteQueryOptions(request); + const fetchRoute = async (routeSearchRequest: RouteSearchRequest) => { + if (routeSearchRequest.source === "address") { + const options = getAddressRouteQueryOptions(routeSearchRequest); return queryClient.fetchQuery({ ...options, @@ -419,7 +444,7 @@ const App = () => { }); } - const options = getCoordinateRouteQueryOptions(request); + const options = getCoordinateRouteQueryOptions(routeSearchRequest); return queryClient.fetchQuery({ ...options, @@ -427,29 +452,29 @@ const App = () => { }); }; - const applyRouteResult = (data: RouteFeatureCollection) => { - setRoutes(data); + const applyRouteSearchResult = (routes: RouteFeatureCollection) => { + setRoutes(routes); setSelectedRouteIndex(null); setSelectedStepIndex(null); - setOriginPosition(data.meta.origin); - setDestinationPosition(data.meta.destination); + setOriginPosition(routes.meta.origin); + setDestinationPosition(routes.meta.destination); setStatusWithTtl("Ready.", "green", 500); }; - const pushSearchHistory = (request: SearchRequest) => { + const pushSearchHistory = (routeSearchRequest: RouteSearchRequest) => { const originLabel = - request.source === "address" - ? request.origin - : formatPointLabel(request.origin); + routeSearchRequest.source === "address" + ? routeSearchRequest.origin + : formatPointLabel(routeSearchRequest.origin); const destinationLabel = - request.source === "address" - ? request.destination - : formatPointLabel(request.destination); + routeSearchRequest.source === "address" + ? routeSearchRequest.destination + : formatPointLabel(routeSearchRequest.destination); const entry: SearchHistoryEntry = { - key: JSON.stringify(buildRouteQueryKey(request)), - request, + key: JSON.stringify(buildRouteQueryKey(routeSearchRequest)), + request: routeSearchRequest, originLabel, destinationLabel, createdAt: new Date().toISOString(), @@ -460,9 +485,9 @@ const App = () => { ); }; - const hasFreshCachedRoute = (request: SearchRequest) => { + const hasFreshCachedRoute = (routeSearchRequest: RouteSearchRequest) => { const queryState = queryClient.getQueryState( - buildRouteQueryKey(request), + buildRouteQueryKey(routeSearchRequest), ); if (!queryState?.dataUpdatedAt) { @@ -476,10 +501,10 @@ const App = () => { return Date.now() - queryState.dataUpdatedAt < routeStaleTimeMs; }; - const executeSearch = async (request: SearchRequest) => { - const canonicalRequest = canonicalizeSearchRequest(request); + const executeSearch = async (routeSearchRequest: RouteSearchRequest) => { + const normalizedRequest = normalizeRouteSearchRequest(routeSearchRequest); const searchId = ++latestSearchIdRef.current; - const cached = hasFreshCachedRoute(canonicalRequest); + const cached = hasFreshCachedRoute(normalizedRequest); if (!cached) { setRouteLoading(true); @@ -487,15 +512,15 @@ const App = () => { } try { - const data = await fetchRoute(canonicalRequest); + const data = await fetchRoute(normalizedRequest); if (searchId !== latestSearchIdRef.current) { return true; } - applyRouteResult(data); - setCommittedSearch(canonicalRequest); - pushSearchHistory(canonicalRequest); + applyRouteSearchResult(data); + setCommittedSearch(normalizedRequest); + pushSearchHistory(normalizedRequest); return true; } catch (error) { @@ -511,14 +536,14 @@ const App = () => { } }; - const searchByAddress = async (origin: string, destination: string) => - executeSearch(buildAddressSearchRequest(origin, destination)); + const runAddressRouteSearch = async (origin: string, destination: string) => + executeSearch(createAddressRouteSearchRequest(origin, destination)); - const searchByCoords = async (origin: Point, destination: Point) => - executeSearch(buildCoordinateSearchRequest(origin, destination)); + const runCoordinateRouteSearch = async (origin: Point, destination: Point) => + executeSearch(createCoordinateRouteSearchRequest(origin, destination)); const scheduleCommittedSearchRefresh = ( - overrides: Partial = {}, + overrides: Partial = {}, ) => { clearScheduledSliderSearch(); @@ -526,104 +551,119 @@ const App = () => { return; } - sliderSearchTimeoutRef.current = window.setTimeout(() => { + sliderSearchTimeoutRef.current = setTimeout(() => { sliderSearchTimeoutRef.current = null; - void executeSearch(rebuildCommittedSearch(committedSearch, overrides)); - }, sliderSearchDelayMs); + void executeSearch(rebuildRouteSearchRequest(committedSearch, overrides)); + }, sliderSearchDelayMs) as unknown as number; }; - const handleTransportModeChange = (nextTransportMode: TransportMode) => { + const handleTravelModeChange = (nextTravelMode: TravelMode) => { clearScheduledSliderSearch(); - setTransportMode(nextTransportMode); + setTravelMode(nextTravelMode); if (!committedSearch) { return; } void executeSearch( - rebuildCommittedSearch(committedSearch, { - transportMode: nextTransportMode, + rebuildRouteSearchRequest(committedSearch, { + travelMode: nextTravelMode, }), ); }; - const handleModeChange = (nextMode: Mode) => { + const handleRoutePlanningModeChange = ( + nextRoutePlanningMode: RoutePlanningMode, + ) => { clearScheduledSliderSearch(); - const nextMethod = resolveMethodForMode( - nextMode, - nextMode === "advanced" && method === "shortest" ? "weighted" : method, + const nextRouteOptimizationMethod = resolveRouteOptimizationMethod( + nextRoutePlanningMode, + nextRoutePlanningMode === "multi-objective" && + routeOptimizationMethod === "shortest" + ? "weighted" + : routeOptimizationMethod, ); - setMode(nextMode); - setMethod(nextMethod); + setRoutePlanningMode(nextRoutePlanningMode); + setRouteOptimizationMethod(nextRouteOptimizationMethod); if (!committedSearch) { return; } void executeSearch( - rebuildCommittedSearch(committedSearch, { - mode: nextMode, - method: nextMethod, + rebuildRouteSearchRequest(committedSearch, { + routePlanningMode: nextRoutePlanningMode, + routeOptimizationMethod: nextRouteOptimizationMethod, }), ); }; - const handleMethodChange = (nextMethod: RouteSelectionMethod) => { + const handleRouteOptimizationMethodChange = ( + nextRouteOptimizationMethod: RouteOptimizationMethod, + ) => { clearScheduledSliderSearch(); - setMethod(nextMethod); + setRouteOptimizationMethod(nextRouteOptimizationMethod); if (!committedSearch) { return; } void executeSearch( - rebuildCommittedSearch(committedSearch, { - method: nextMethod, + rebuildRouteSearchRequest(committedSearch, { + routeOptimizationMethod: nextRouteOptimizationMethod, }), ); }; - const handleScenicChangeEnd = (nextScenic: number) => { - setScenic(nextScenic); - scheduleCommittedSearchRefresh({ scenic: nextScenic }); + const handleScenicWeightChangeEnd = (nextScenicWeight: number) => { + setScenicWeight(nextScenicWeight); + scheduleCommittedSearchRefresh({ scenicWeight: nextScenicWeight }); }; - const handleSnowChangeEnd = (nextSnow: number) => { - setSnow(nextSnow); - scheduleCommittedSearchRefresh({ snow: nextSnow }); + const handleSnowFreeWeightChangeEnd = (nextSnowFreeWeight: number) => { + setSnowFreeWeight(nextSnowFreeWeight); + scheduleCommittedSearchRefresh({ snowFreeWeight: nextSnowFreeWeight }); }; - const handleUphillChangeEnd = (nextUphill: number) => { - setUphill(nextUphill); - scheduleCommittedSearchRefresh({ uphill: nextUphill }); + const handleFlatWeightChangeEnd = (nextFlatWeight: number) => { + setFlatWeight(nextFlatWeight); + scheduleCommittedSearchRefresh({ flatWeight: nextFlatWeight }); }; - const handleSelectHistoryEntry = async (entry: SearchHistoryEntry) => { + const handleSelectHistoryEntry = async ( + searchHistoryEntry: SearchHistoryEntry, + ) => { clearScheduledSliderSearch(); - setTransportMode(entry.request.transportMode); - setMode(entry.request.mode); - setMethod(entry.request.method); - setScenic(entry.request.scenic); - setSnow(entry.request.snow); - setUphill(entry.request.uphill); - - if (entry.request.source === "address") { - routePanelRef.current?.setOrigin(entry.request.origin); - routePanelRef.current?.setDestination(entry.request.destination); + setTravelMode(searchHistoryEntry.request.travelMode); + setRoutePlanningMode(searchHistoryEntry.request.routePlanningMode); + setRouteOptimizationMethod( + searchHistoryEntry.request.routeOptimizationMethod, + ); + setScenicWeight(searchHistoryEntry.request.scenicWeight); + setSnowFreeWeight(searchHistoryEntry.request.snowFreeWeight); + setFlatWeight(searchHistoryEntry.request.flatWeight); + + if (searchHistoryEntry.request.source === "address") { + routePanelRef.current?.setOrigin(searchHistoryEntry.request.origin); + routePanelRef.current?.setDestination( + searchHistoryEntry.request.destination, + ); } else { - setOriginPosition(entry.request.origin); - setDestinationPosition(entry.request.destination); - routePanelRef.current?.setOrigin(entry.originLabel); - routePanelRef.current?.setDestination(entry.destinationLabel); + setOriginPosition(searchHistoryEntry.request.origin); + setDestinationPosition(searchHistoryEntry.request.destination); + routePanelRef.current?.setOrigin(searchHistoryEntry.originLabel); + routePanelRef.current?.setDestination( + searchHistoryEntry.destinationLabel, + ); } - await executeSearch(entry.request); + await executeSearch(searchHistoryEntry.request); }; - const reverseGeocode = async (kind: MarkerKind, point: Point) => { + const reverseGeocode = async (routeEndpoint: RouteEndpoint, point: Point) => { const [lon, lat] = point.coordinates; try { @@ -637,7 +677,7 @@ const App = () => { return; } - if (kind === "origin") { + if (routeEndpoint === "origin") { routePanelRef.current?.setOrigin(data.address); return; @@ -673,11 +713,14 @@ const App = () => { } }; - const getMarkerPosition = (kind: MarkerKind) => - kind === "origin" ? originPosition : destinationPosition; + const getMarkerPosition = (routeEndpoint: RouteEndpoint) => + routeEndpoint === "origin" ? originPosition : destinationPosition; - const setMarkerPosition = (kind: MarkerKind, point: Point | null) => { - if (kind === "origin") { + const setMarkerPosition = ( + routeEndpoint: RouteEndpoint, + point: Point | null, + ) => { + if (routeEndpoint === "origin") { setOriginPosition(point); return; @@ -686,15 +729,15 @@ const App = () => { setDestinationPosition(point); }; - const getOtherMarkerPosition = (kind: MarkerKind) => - kind === "origin" ? destinationPosition : originPosition; + const getOtherMarkerPosition = (routeEndpoint: RouteEndpoint) => + routeEndpoint === "origin" ? destinationPosition : originPosition; const applyMarkerUpdate = async ( - kind: MarkerKind, + routeEndpoint: RouteEndpoint, position: Point, source: MarkerSource, ) => { - const markerName = kind === "origin" ? "Origin" : "Destination"; + const markerName = routeEndpoint === "origin" ? "Origin" : "Destination"; if (!boundary || !isInsideBoundary(position, boundary)) { setStatusWithTtl( @@ -706,24 +749,25 @@ const App = () => { return false; } - const previousPosition = getMarkerPosition(kind); - const otherMarkerPosition = getOtherMarkerPosition(kind); + const previousPosition = getMarkerPosition(routeEndpoint); + const otherMarkerPosition = getOtherMarkerPosition(routeEndpoint); if (source === "pick") { - setPickMode(null); + setActiveRouteEndpoint(null); } - setMarkerPosition(kind, position); - await reverseGeocode(kind, position); + setMarkerPosition(routeEndpoint, position); + await reverseGeocode(routeEndpoint, position); if (!otherMarkerPosition) { return true; } - const origin = kind === "origin" ? position : otherMarkerPosition; - const destination = kind === "destination" ? position : otherMarkerPosition; + const origin = routeEndpoint === "origin" ? position : otherMarkerPosition; + const destination = + routeEndpoint === "destination" ? position : otherMarkerPosition; - const ok = await searchByCoords(origin, destination); + const ok = await runCoordinateRouteSearch(origin, destination); if (ok) { return true; @@ -731,7 +775,7 @@ const App = () => { setStatusWithTtl(`${markerName} marker could not be set.`, "red", 2000); - setMarkerPosition(kind, previousPosition ?? null); + setMarkerPosition(routeEndpoint, previousPosition ?? null); return false; }; @@ -749,19 +793,21 @@ const App = () => { applyMarkerUpdate("destination", position, "pick"); const onTogglePickOrigin = () => { - setPickMode((pickMode) => (pickMode === "origin" ? null : "origin")); + setActiveRouteEndpoint((activeRouteEndpoint) => + activeRouteEndpoint === "origin" ? null : "origin", + ); }; const onTogglePickDestination = () => { - setPickMode((pickMode) => - pickMode === "destination" ? null : "destination", + setActiveRouteEndpoint((activeRouteEndpoint) => + activeRouteEndpoint === "destination" ? null : "destination", ); }; const handleClearAll = () => { clearScheduledSliderSearch(); - setPickMode(null); + setActiveRouteEndpoint(null); setOriginPosition(null); setDestinationPosition(null); setRoutes(undefined); @@ -776,21 +822,21 @@ const App = () => { }; useEffect(() => { - if (pickMode === "origin") { + if (activeRouteEndpoint === "origin") { setStatusWithTtl("Click on the map to place the origin marker.", "grape"); } - if (pickMode === "destination") { + if (activeRouteEndpoint === "destination") { setStatusWithTtl( "Click on the map to place the destination marker.", "grape", ); } - if (!pickMode) { + if (!activeRouteEndpoint) { // setStatusWithTtl("Ready.", "green"); } - }, [destinationPosition, pickMode, originPosition]); + }, [destinationPosition, activeRouteEndpoint, originPosition]); if (boundaryLoading) { return ( @@ -842,14 +888,14 @@ const App = () => { onOriginDragged={onOriginDragged} onDestinationDragged={onDestinationDragged} selectedStepIndex={selectedStepIndex} - pickMode={pickMode} + activeRouteEndpoint={activeRouteEndpoint} onPickOrigin={onPickOrigin} onPickDestination={onPickDestination} - transportMode={transportMode} + travelMode={travelMode} /> { onSelectStepIndex={setSelectedStepIndex} onTogglePickOrigin={onTogglePickOrigin} onTogglePickDestination={onTogglePickDestination} - pickMode={pickMode} + activeRouteEndpoint={activeRouteEndpoint} hasOriginMarker={originPosition !== null} hasDestinationMarker={destinationPosition !== null} onClearAll={handleClearAll} - transportMode={transportMode} - onTransportModeChange={handleTransportModeChange} - mode={mode} - onModeChange={handleModeChange} - method={method} - onMethodChange={handleMethodChange} - scenic={scenic} - onScenicChange={setScenic} - onScenicChangeEnd={handleScenicChangeEnd} - snow={snow} - onSnowChange={setSnow} - onSnowChangeEnd={handleSnowChangeEnd} - uphill={uphill} - onUphillChange={setUphill} - onUphillChangeEnd={handleUphillChangeEnd} + travelMode={travelMode} + onTravelModeChange={handleTravelModeChange} + routePlanningMode={routePlanningMode} + onRoutePlanningModeChange={handleRoutePlanningModeChange} + routeOptimizationMethod={routeOptimizationMethod} + onRouteOptimizationMethodChange={handleRouteOptimizationMethodChange} + scenicWeight={scenicWeight} + onScenicWeightChange={setScenicWeight} + onScenicWeightChangeEnd={handleScenicWeightChangeEnd} + snowFreeWeight={snowFreeWeight} + onSnowFreeWeightChange={setSnowFreeWeight} + onSnowFreeWeightChangeEnd={handleSnowFreeWeightChangeEnd} + flatWeight={flatWeight} + onFlatWeightChange={setFlatWeight} + onFlatWeightChangeEnd={handleFlatWeightChangeEnd} searchHistory={searchHistory} onSelectHistoryEntry={handleSelectHistoryEntry} onClearHistory={removeSearchHistory} /> - + ); }; diff --git a/frontend/src/Map.tsx b/frontend/src/Map.tsx index 5413f17..1cae4d6 100644 --- a/frontend/src/Map.tsx +++ b/frontend/src/Map.tsx @@ -20,12 +20,11 @@ import type { } from "@/client"; import type { FeatureCollection } from "geojson"; import { toGeoJsonObject } from "@/utils.ts"; -import { RouteBoundsController } from "@/RouteBoundsController.tsx"; -import type { PickMode } from "@/RoutePanel.tsx"; +import { RouteViewportController } from "@/RouteViewportController.tsx"; import { MarkerPickController } from "@/MarkerPickController.tsx"; import { basePadding, leftMargin } from "@/constants.ts"; -import { AttributeOverlay } from "@/AttributeOverlay.tsx"; -import type { TransportMode } from "@/types/global.ts"; +import { OverlayLayer } from "@/OverlayLayer.tsx"; +import type { ActiveRouteEndpoint, TravelMode } from "@/types/global.ts"; import { Box } from "@mantine/core"; interface MapProps { @@ -37,13 +36,13 @@ interface MapProps { onOriginDragged: (position: Point) => Promise; onDestinationDragged: (position: Point) => Promise; selectedStepIndex: number | null; - pickMode: PickMode; + activeRouteEndpoint: ActiveRouteEndpoint; onPickOrigin: (point: Point) => Promise; onPickDestination: (point: Point) => Promise; - transportMode: TransportMode; + travelMode: TravelMode; } -const extractOuterRingsAsHoles = ( +const extractBoundaryMaskHoles = ( geometry: MultiPolygon, ): number[][][] | null => { const coords = geometry.coordinates; @@ -72,20 +71,20 @@ export const Map = ({ onOriginDragged, onDestinationDragged, selectedStepIndex, - pickMode, + activeRouteEndpoint, onPickOrigin, onPickDestination, - transportMode, + travelMode, }: MapProps) => { - const [minLong, minLat, maxLong, maxLat] = boundary.meta.bounds; - const initialBounds = L.latLngBounds([minLat, minLong], [maxLat, maxLong]); + const [minLon, minLat, maxLon, maxLat] = boundary.meta.bounds; + const initialBounds = L.latLngBounds([minLat, minLon], [maxLat, maxLon]); const selectedRoute: RouteFeature | null = routes && selectedRouteIndex != null ? (routes.features[selectedRouteIndex] ?? null) : null; - const visibleRoutes = + const displayedRoutes = routes && selectedRoute ? { ...routes, @@ -98,13 +97,19 @@ export const Map = ({ ? (selectedRoute.properties.steps[selectedStepIndex] ?? null) : null; - const maskGeoJson = useMemo(() => { + const boundaryMaskGeoJson = useMemo(() => { if (boundary.features.length === 0) { return null; } const geometry = boundary.features[0].geometry; + const holes = extractBoundaryMaskHoles(geometry); + + if (!holes) { + return null; + } + const worldRing = [ [-180, -90], [-180, 90], @@ -113,12 +118,6 @@ export const Map = ({ [-180, -90], ]; - const holes = extractOuterRingsAsHoles(geometry); - - if (!holes) { - return null; - } - return { type: "FeatureCollection", features: [ @@ -151,9 +150,9 @@ export const Map = ({ attribution='© OpenStreetMap contributors' url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> - {maskGeoJson && ( + {boundaryMaskGeoJson && ( ({ color: "#000", @@ -172,12 +171,12 @@ export const Map = ({ fillOpacity: 0, })} /> - @@ -191,37 +190,28 @@ export const Map = ({ {selectedRoute && ( )} - + - + - + - + - + - + - + - + diff --git a/frontend/src/MarkerPickController.tsx b/frontend/src/MarkerPickController.tsx index 3b9539b..5904ef6 100644 --- a/frontend/src/MarkerPickController.tsx +++ b/frontend/src/MarkerPickController.tsx @@ -1,21 +1,21 @@ -import type { PickMode } from "@/RoutePanel.tsx"; import type { Point } from "@/client"; import { useMapEvents } from "react-leaflet"; +import type { ActiveRouteEndpoint } from "@/types/global.ts"; interface MarkerPickControllerProps { - pickMode: PickMode; + activeRouteEndpoint: ActiveRouteEndpoint; onPickOrigin: (point: Point) => Promise; onPickDestination: (point: Point) => Promise; } export const MarkerPickController = ({ - pickMode, + activeRouteEndpoint, onPickOrigin, onPickDestination, }: MarkerPickControllerProps) => { useMapEvents({ click: (leafletMouseEvent) => { - if (!pickMode) { + if (!activeRouteEndpoint) { return; } @@ -27,7 +27,7 @@ export const MarkerPickController = ({ ], }; - if (pickMode === "origin") { + if (activeRouteEndpoint === "origin") { void onPickOrigin(point); return; diff --git a/frontend/src/OriginDestinationMarkers.tsx b/frontend/src/OriginDestinationMarkers.tsx index 4fed584..0071d7b 100644 --- a/frontend/src/OriginDestinationMarkers.tsx +++ b/frontend/src/OriginDestinationMarkers.tsx @@ -59,7 +59,7 @@ export const OriginDestinationMarkers = ({ }, }} > - End + Destination )} diff --git a/frontend/src/AttributeOverlay.tsx b/frontend/src/OverlayLayer.tsx similarity index 55% rename from frontend/src/AttributeOverlay.tsx rename to frontend/src/OverlayLayer.tsx index 5fccda5..a384e6d 100644 --- a/frontend/src/AttributeOverlay.tsx +++ b/frontend/src/OverlayLayer.tsx @@ -5,49 +5,51 @@ import { useMap, useMapEvents, } from "react-leaflet"; -import type { OverlayAttribute, TransportMode } from "@/types/global.ts"; +import type { MapOverlayKey, TravelMode } from "@/types/global.ts"; import { useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; -import { listLayersLayersGetOptions } from "@/client/@tanstack/react-query.gen.ts"; +import { listOverlayFeaturesLayersGetOptions } from "@/client/@tanstack/react-query.gen.ts"; import { capitalize, toGeoJsonObject } from "@/utils.ts"; import { Text } from "@mantine/core"; import type { Feature } from "geojson"; import L from "leaflet"; -interface AttributeOverlayProps { - overlayAttribute: OverlayAttribute; - transportMode: TransportMode; +interface OverlayLayerProps { + mapOverlayKey: MapOverlayKey; + travelMode: TravelMode; } -const boundsToBbox = (map: L.Map) => { +const mapBoundsToBoundingBox = (map: L.Map) => { const bounds = map.getBounds(); return `${String(bounds.getWest())},${String(bounds.getSouth())},${String(bounds.getEast())},${String(bounds.getNorth())}`; }; -export const AttributeOverlay = ({ - overlayAttribute, - transportMode, -}: AttributeOverlayProps) => { +export const OverlayLayer = ({ + mapOverlayKey, + travelMode, +}: OverlayLayerProps) => { const map = useMap(); - const [bbox, setBbox] = useState(() => boundsToBbox(map)); + const [boundingBox, setBoundingBox] = useState(() => + mapBoundsToBoundingBox(map), + ); useMapEvents({ moveend: () => { - setBbox(boundsToBbox(map)); + setBoundingBox(mapBoundsToBoundingBox(map)); }, zoomend: () => { - setBbox(boundsToBbox(map)); + setBoundingBox(mapBoundsToBoundingBox(map)); }, }); - const layerQuery = useQuery({ - ...listLayersLayersGetOptions({ + const overlayQuery = useQuery({ + ...listOverlayFeaturesLayersGetOptions({ query: { - overlay_attribute: overlayAttribute, - transport_mode: transportMode, - bounding_box: bbox, + overlay_key: mapOverlayKey, + travel_mode: travelMode, + bounding_box: boundingBox, minimum_value: 0.01, max_features: 20000, }, @@ -59,14 +61,14 @@ export const AttributeOverlay = ({ const style = useMemo(() => { let color = "#ffffff"; - switch (overlayAttribute) { + switch (mapOverlayKey) { case "snow": color = "#fa4561"; break; case "scenic": color = "#4dfa8c"; break; - case "uphill": + case "hills": color = "#a652ff"; break; } @@ -82,19 +84,19 @@ export const AttributeOverlay = ({ opacity, }; }; - }, [overlayAttribute]); + }, [mapOverlayKey]); - if (layerQuery.isLoading || !layerQuery.data) { + if (overlayQuery.isLoading || !overlayQuery.data) { return null; } return ( - {capitalize(overlayAttribute)} area - {/*Intensity: {layerQuery.data.features[0].properties.value}/1*/} + {capitalize(mapOverlayKey)} area + {/*Intensity: {overlayQuery.data.features[0].properties.value}/1*/} - + ); }; diff --git a/frontend/src/RouteStatsPanel.tsx b/frontend/src/RouteAnalysisPanel.tsx similarity index 71% rename from frontend/src/RouteStatsPanel.tsx rename to frontend/src/RouteAnalysisPanel.tsx index 6d484cb..9685cd9 100644 --- a/frontend/src/RouteStatsPanel.tsx +++ b/frontend/src/RouteAnalysisPanel.tsx @@ -23,16 +23,17 @@ import { YAxis, } from "recharts"; import type { RouteFeature, RouteFeatureCollection } from "@/client"; -import { panelWidth, statsPanelWidth } from "@/constants.ts"; +import { routePanelWidth, analysisPanelWidth } from "@/constants.ts"; import { - getRouteComfortScores, - objectiveScoreLabels, + getRouteScores, + routeScoreLabels, + type RouteScores, } from "@/route-metrics.ts"; import { getRouteColor } from "@/utils.ts"; -type ObjectiveAxis = "snowlessComfort" | "uphillComfort" | "scenicComfort"; +type ScoreAxis = keyof RouteScores; -interface RouteStatsPanelProps { +interface RouteAnalysisPanelProps { routes: RouteFeatureCollection; selectedRouteIndex: number | null; onClose: () => void; @@ -42,24 +43,18 @@ interface RouteStatsPanelProps { maxHeight?: string; } -interface RouteStatsDatum { +interface RouteScoreSummary { routeIndex: number; routeLabel: string; color: string; - snowlessComfort: number; - uphillComfort: number; - scenicComfort: number; + snowFreeScore: number; + flatScore: number; + scenicScore: number; selected: boolean; } type TooltipValue = string | number | readonly (string | number)[] | undefined; -const objectiveLabels: Record = { - snowlessComfort: objectiveScoreLabels.snowlessComfort, - uphillComfort: objectiveScoreLabels.uphillComfort, - scenicComfort: objectiveScoreLabels.scenicComfort, -}; - const getRouteLabel = (route: RouteFeature, routeCount: number) => { if (routeCount === 1) { return "Route"; @@ -72,21 +67,21 @@ const getRouteLabel = (route: RouteFeature, routeCount: number) => { return `Route ${String(route.properties.route_index + 1)}`; }; -const buildRouteStatsData = ( +const buildRouteAnalysisData = ( routeCollection: RouteFeatureCollection, selectedRouteIndex: number | null, -): RouteStatsDatum[] => +): RouteScoreSummary[] => routeCollection.features - .filter((route) => route.properties.objective_costs != null) + .filter((route) => route.properties.penalty_breakdown != null) .map((route) => { const routeIndex = route.properties.route_index; - const comfortScores = getRouteComfortScores(route); + const routeScores = getRouteScores(route); return { routeIndex, routeLabel: getRouteLabel(route, routeCollection.features.length), color: getRouteColor(routeIndex), - ...comfortScores, + ...routeScores, selected: selectedRouteIndex === routeIndex, }; }); @@ -101,16 +96,16 @@ const formatTooltipValue = (value: TooltipValue) => { return formatPercentTick(numericValue); }; -const ScatterComparisonChart = ({ +const RouteScoreScatterPlot = ({ data, title, xKey, yKey, }: { - data: RouteStatsDatum[]; + data: RouteScoreSummary[]; title: string; - xKey: ObjectiveAxis; - yKey: ObjectiveAxis; + xKey: ScoreAxis; + yKey: ScoreAxis; }) => ( @@ -126,11 +121,11 @@ const ScatterComparisonChart = ({ type="number" dataKey={xKey} label={{ - value: objectiveLabels[xKey], + value: routeScoreLabels[xKey], position: "insideBottom", offset: -10, }} - name={objectiveLabels[xKey]} + name={routeScoreLabels[xKey]} domain={[0, 100]} tickFormatter={formatPercentTick} /> @@ -138,12 +133,13 @@ const ScatterComparisonChart = ({ type="number" dataKey={yKey} label={{ - value: objectiveLabels[yKey], + value: routeScoreLabels[yKey], position: "insideLeft", + textAnchor: "middle", angle: -90, offset: 5, }} - name={objectiveLabels[yKey]} + name={routeScoreLabels[yKey]} domain={[0, 100]} // width="auto" tickFormatter={formatPercentTick} @@ -166,16 +162,17 @@ const ScatterComparisonChart = ({ ); -export const RouteStatsPanel = ({ +export const RouteAnalysisPanel = ({ routes, selectedRouteIndex, onClose, style, top = 12, - left = panelWidth + 20, + left = routePanelWidth + 20, maxHeight = "calc(100dvh - 90px)", -}: RouteStatsPanelProps) => { - const chartData = buildRouteStatsData(routes, selectedRouteIndex); +}: RouteAnalysisPanelProps) => { + const analysisData = buildRouteAnalysisData(routes, selectedRouteIndex); + const routeCount = routes.features.length; return ( - Route Statistics + + Route {routeCount === 1 ? "Analysis" : "Comparison"} + - Normalized to 0-100%, higher is better. + Higher is better - {chartData.length > 0 ? ( + {analysisData.length > 0 ? ( - {chartData.map((route) => ( + {analysisData.map((route) => ( {route.selected && ( - Focused + Selected )} @@ -241,13 +240,12 @@ export const RouteStatsPanel = ({ - Route Score Comparison + Score {routeCount === 1 ? "Analysis" : "Comparison"} @@ -256,22 +254,22 @@ export const RouteStatsPanel = ({ {/* TODO Match the fill colors to the score badge colors */} - - - ) : ( - Route statistics are not available for the current result. + Route {routeCount === 1 ? "analysis" : "comparison"} is not + available for the current result. )} diff --git a/frontend/src/RouteLayer.tsx b/frontend/src/RouteLayer.tsx index 0662223..e0ff930 100644 --- a/frontend/src/RouteLayer.tsx +++ b/frontend/src/RouteLayer.tsx @@ -15,8 +15,8 @@ export const RouteLayer = ({ routes }: RouteLayerProps) => { return routes.features.map((route) => ( - L.latLng(position[1], position[0]), + positions={route.geometry.coordinates.map((coordinate) => + L.latLng(coordinate[1], coordinate[0]), )} pathOptions={{ color: getRouteColor(route.properties.route_index), diff --git a/frontend/src/RoutePanel.tsx b/frontend/src/RoutePanel.tsx index c887079..13451c3 100644 --- a/frontend/src/RoutePanel.tsx +++ b/frontend/src/RoutePanel.tsx @@ -31,33 +31,35 @@ import { import type { RouteFeature, RouteFeatureCollection, - RouteStepResponse, + RouteStepSummary, } from "@/client"; import markerIconUrl from "leaflet/dist/images/marker-icon.png?url"; -import type { TransportMode } from "@/types/global.ts"; -import type { RouteSelectionMethod, Mode, SearchHistoryEntry } from "@/App.tsx"; +import type { ActiveRouteEndpoint, TravelMode } from "@/types/global.ts"; +import type { + RouteOptimizationMethod, + RoutePlanningMode, + SearchHistoryEntry, +} from "@/App.tsx"; import { - buildRouteProgressionData, + buildRouteScoreProfile, estimateRouteDurationMinutes, formatDuration, - getRouteComfortScores, - getRouteSpeedKmPerHour, - objectiveScoreLabels, + getRouteScores, + getTravelSpeedKmh, + routeScoreLabels, } from "@/route-metrics.ts"; import { getRouteColor } from "@/utils.ts"; -import { RouteStatsPanel } from "@/RouteStatsPanel.tsx"; +import { RouteAnalysisPanel } from "@/RouteAnalysisPanel.tsx"; import { CartesianGrid, - Tooltip as RechartsTooltip, + Legend, Line, LineChart, + Tooltip as RechartsTooltip, XAxis, YAxis, - Legend, } from "recharts"; -export type PickMode = "origin" | "destination" | null; - export interface RoutePanelHandle { setOrigin: (origin: string) => void; setDestination: (destination: string) => void; @@ -83,22 +85,24 @@ interface RoutePanelProps { onClearAll: () => void; hasOriginMarker: boolean; hasDestinationMarker: boolean; - pickMode: PickMode; - transportMode: TransportMode; - onTransportModeChange: (transportMode: TransportMode) => void; - mode: Mode; - onModeChange: (mode: Mode) => void; - method: RouteSelectionMethod; - onMethodChange: (method: RouteSelectionMethod) => void; - scenic: number; - onScenicChange: (scenic: number) => void; - onScenicChangeEnd: (scenic: number) => void; - snow: number; - onSnowChange: (snow: number) => void; - onSnowChangeEnd: (snow: number) => void; - uphill: number; - onUphillChange: (uphill: number) => void; - onUphillChangeEnd: (uphill: number) => void; + activeRouteEndpoint: ActiveRouteEndpoint; + travelMode: TravelMode; + onTravelModeChange: (travelMode: TravelMode) => void; + routePlanningMode: RoutePlanningMode; + onRoutePlanningModeChange: (routePlanningMode: RoutePlanningMode) => void; + routeOptimizationMethod: RouteOptimizationMethod; + onRouteOptimizationMethodChange: ( + routeOptimizationMethod: RouteOptimizationMethod, + ) => void; + scenicWeight: number; + onScenicWeightChange: (scenicWeight: number) => void; + onScenicWeightChangeEnd: (scenicWeight: number) => void; + snowFreeWeight: number; + onSnowFreeWeightChange: (snowFreeWeight: number) => void; + onSnowFreeWeightChangeEnd: (snowFreeWeight: number) => void; + flatWeight: number; + onFlatWeightChange: (flatWeight: number) => void; + onFlatWeightChangeEnd: (flatWeight: number) => void; searchHistory: SearchHistoryEntry[]; onSelectHistoryEntry: (entry: SearchHistoryEntry) => Promise; onClearHistory: () => void; @@ -115,9 +119,9 @@ const distanceToText = (distance: number) => { const getRouteTitle = ( route: RouteFeature, routeCount: number, - method: RouteSelectionMethod, + routeOptimizationMethod: RouteOptimizationMethod, ) => { - if (method === "pareto") { + if (routeOptimizationMethod === "pareto") { const routeRank = route.properties.pareto_rank ?? route.properties.route_index + 1; @@ -142,37 +146,37 @@ const formatTooltipPercent = (value: TooltipValue) => { return formatPercent(numericValue); }; -const objectiveBadgeConfig = [ +const scoreBadgeConfig = [ { - key: "uphillComfort", - label: objectiveScoreLabels.uphillComfort, + key: "flatScore", + label: routeScoreLabels.flatScore, color: "green", }, { - key: "scenicComfort", - label: objectiveScoreLabels.scenicComfort, + key: "scenicScore", + label: routeScoreLabels.scenicScore, color: "orange", }, { - key: "snowlessComfort", - label: objectiveScoreLabels.snowlessComfort, + key: "snowFreeScore", + label: routeScoreLabels.snowFreeScore, color: "cyan", }, ] as const; -const RouteObjectiveBadges = ({ route }: { route: RouteFeature }) => { - const comfortScores = getRouteComfortScores(route); +const RouteScoreBadges = ({ route }: { route: RouteFeature }) => { + const routeScores = getRouteScores(route); return ( - {objectiveBadgeConfig.map((objective) => ( + {scoreBadgeConfig.map((scoreBadge) => ( - {objective.label} {formatPercent(comfortScores[objective.key])} + {scoreBadge.label} {formatPercent(routeScores[scoreBadge.key])} ))} @@ -184,7 +188,7 @@ const RouteStepList = ({ selectedStepIndex, onSelectStepIndex, }: { - steps: RouteStepResponse[]; + steps: RouteStepSummary[]; selectedStepIndex: number | null; onSelectStepIndex: (index: number | null) => void; }) => ( @@ -242,51 +246,55 @@ export const RoutePanel = ({ onTogglePickDestination, hasOriginMarker, hasDestinationMarker, - pickMode, + activeRouteEndpoint, onClearAll, - transportMode, - onTransportModeChange, - mode, - onModeChange, - method, - onMethodChange, - scenic, - onScenicChange, - onScenicChangeEnd, - snow, - onSnowChange, - onSnowChangeEnd, - uphill, - onUphillChange, - onUphillChangeEnd, + travelMode, + onTravelModeChange, + routePlanningMode, + onRoutePlanningModeChange, + routeOptimizationMethod, + onRouteOptimizationMethodChange, + scenicWeight, + onScenicWeightChange, + onScenicWeightChangeEnd, + snowFreeWeight, + onSnowFreeWeightChange, + onSnowFreeWeightChangeEnd, + flatWeight, + onFlatWeightChange, + onFlatWeightChangeEnd, searchHistory, onSelectHistoryEntry, onClearHistory, }: RoutePanelProps) => { + const [isAnalysisPanelOpen, setIsAnalysisPanelOpen] = useState(false); + const [walkingSpeedKmh, setWalkingSpeedKmh] = useState(5); + const [cyclingSpeedKmh, setCyclingSpeedKmh] = useState(15); + const routeList = routes?.features ?? []; const routeCount = routeList.length; const hasRoute = routeCount > 0; - const shouldConstrainPanel = mode === "advanced" || hasRoute; - const detailOpen = selectedRouteIndex != null && selectedRoute != null; + const shouldConstrainPanel = + routePlanningMode === "multi-objective" || hasRoute; + const isRouteDetailOpen = selectedRouteIndex != null && selectedRoute != null; const clearDisabled = !hasOriginMarker && !hasDestinationMarker; const selectedRouteSteps = selectedRoute?.properties.steps ?? []; - const [statsOpen, setStatsOpen] = useState(false); - const statsVisible = statsOpen && routes != null; - const [walkingSpeed, setWalkingSpeed] = useState(5); - const [bikingSpeed, setBikingSpeed] = useState(15); - const routeSpeedKmPerHour = getRouteSpeedKmPerHour( - transportMode, - walkingSpeed, - bikingSpeed, + const isAnalysisPanelVisible = isAnalysisPanelOpen && routes != null; + + const travelSpeedKmh = getTravelSpeedKmh( + travelMode, + walkingSpeedKmh, + cyclingSpeedKmh, ); - const getEstimatedTimeLabel = (distanceMeters: number) => + + const formatEstimatedDuration = (distanceMeters: number) => formatDuration( - estimateRouteDurationMinutes(distanceMeters, routeSpeedKmPerHour), + estimateRouteDurationMinutes(distanceMeters, travelSpeedKmh), ); - const selectedRouteProgressionData = - buildRouteProgressionData(selectedRouteSteps); - const form = useForm({ + const selectedRouteScoreProfile = buildRouteScoreProfile(selectedRouteSteps); + + const searchForm = useForm({ mode: "uncontrolled", initialValues: { origin: "", @@ -298,20 +306,20 @@ export const RoutePanel = ({ ref, () => ({ setOrigin: (origin: string) => { - form.setFieldValue("origin", origin); + searchForm.setFieldValue("origin", origin); }, setDestination: (destination: string) => { - form.setFieldValue("destination", destination); + searchForm.setFieldValue("destination", destination); }, clearAllFields: () => { - form.setFieldValue("origin", ""); - form.setFieldValue("destination", ""); + searchForm.setFieldValue("origin", ""); + searchForm.setFieldValue("destination", ""); }, }), - [form], + [searchForm], ); - const routeOverviewList = ( + const routeCards = ( {routeList.map((route) => { const routeIndex = route.properties.route_index; @@ -340,7 +348,11 @@ export const RoutePanel = ({ - {getRouteTitle(route, routeCount, method)} + {getRouteTitle( + route, + routeCount, + routeOptimizationMethod, + )} {recommendedRoute && ( @@ -351,9 +363,9 @@ export const RoutePanel = ({ - {getEstimatedTimeLabel(route.properties.distance)} + {formatEstimatedDuration(route.properties.distance)} - + @@ -375,14 +387,14 @@ export const RoutePanel = ({ - {getRouteTitle(selectedRoute, routeCount, method)} + {getRouteTitle(selectedRoute, routeCount, routeOptimizationMethod)} {selectedRouteSteps.length > 0 ? ( - Info + Overview Total Distance @@ -394,12 +406,12 @@ export const RoutePanel = ({ Estimated Time - Based on {String(routeSpeedKmPerHour)} km/h average{" "} - {transportMode === "walk" ? "walking" : "biking"} speed + Based on {String(travelSpeedKmh)} km/h average{" "} + {travelMode === "walking" ? "walking" : "cycling"} speed - {getEstimatedTimeLabel(selectedRoute.properties.distance)} + {formatEstimatedDuration(selectedRoute.properties.distance)} @@ -407,15 +419,15 @@ export const RoutePanel = ({ Higher is better - + - Progression + Score Profile - Scores throughout the route + Scores along the route ); - const routeListSection = hasRoute ? ( + const routeResultsSection = hasRoute ? ( <> @@ -499,24 +511,24 @@ export const RoutePanel = ({ - {routeOverviewList} + {routeCards} ) : null; const searchPane = ( - +
{ + onSubmit={searchForm.onSubmit((values) => { void searchByAddress(values.origin, values.destination); })} > @@ -526,13 +538,13 @@ export const RoutePanel = ({ placeholder="Origin" mt="md" flex={1} - key={form.key("origin")} - {...form.getInputProps("origin")} + key={searchForm.key("origin")} + {...searchForm.getInputProps("origin")} /> @@ -546,13 +558,15 @@ export const RoutePanel = ({ placeholder="Destination" mt="md" flex={1} - key={form.key("destination")} - {...form.getInputProps("destination")} + key={searchForm.key("destination")} + {...searchForm.getInputProps("destination")} /> @@ -572,22 +586,22 @@ export const RoutePanel = ({ - Transport + Travel Mode { - onTransportModeChange(value as TransportMode); + onTravelModeChange(value as TravelMode); }} fullWidth data={[ - { label: "Walking", value: "walk" }, - { label: "Bike", value: "bike" }, + { label: "Walking", value: "walking" }, + { label: "Cycling", value: "cycling" }, ]} /> - Mode + Optimization { - onModeChange(value as Mode); + onRoutePlanningModeChange(value as RoutePlanningMode); }} fullWidth data={[ { label: "Shortest", value: "shortest" }, - { label: "Advanced", value: "advanced" }, + { label: "Multi-objective", value: "multi-objective" }, ]} /> - {mode === "advanced" && ( + {routePlanningMode === "multi-objective" && ( <> { - onMethodChange(value as RouteSelectionMethod); + onRouteOptimizationMethodChange( + value as RouteOptimizationMethod, + ); }} fullWidth mt="md" @@ -626,15 +642,15 @@ export const RoutePanel = ({ ]} /> - Scenic + Prefer Scenic - Avoid Snow + Prefer Snow-free - Avoid Uphill + Avoid Hills {loading ? "Searching..." : "Search"} - {routeListSection} + {routeResultsSection}
); @@ -693,7 +713,7 @@ export const RoutePanel = ({ - {entry.request.transportMode === "walk" ? "Walk" : "Bike"} + {entry.request.travelMode === "walking" + ? "Walking" + : "Cycling"} - {entry.request.mode === "shortest" + {entry.request.routePlanningMode === "shortest" ? "Shortest" - : entry.request.method === "weighted" + : entry.request.routeOptimizationMethod === "weighted" ? "Weighted" : "Pareto"} @@ -787,8 +809,8 @@ export const RoutePanel = ({ size="xl" mt="sm" mb="lg" - value={walkingSpeed} - onChange={setWalkingSpeed} + value={walkingSpeedKmh} + onChange={setWalkingSpeedKmh} domain={[1, 10]} min={1} max={10} @@ -798,14 +820,14 @@ export const RoutePanel = ({ { value: 10, label: "10" }, ]} /> - Biking Speed (in km/h) + Cycling Speed (in km/h) {tabs} )}
- + {(styles) => routes ? ( - { - setStatsOpen(false); + setIsAnalysisPanelOpen(false); }} maxHeight="calc(100dvh - 90px)" style={styles} diff --git a/frontend/src/RouteBoundsController.tsx b/frontend/src/RouteViewportController.tsx similarity index 59% rename from frontend/src/RouteBoundsController.tsx rename to frontend/src/RouteViewportController.tsx index 0cd004a..0089863 100644 --- a/frontend/src/RouteBoundsController.tsx +++ b/frontend/src/RouteViewportController.tsx @@ -1,47 +1,47 @@ import { useMap } from "react-leaflet"; import L from "leaflet"; -import { fitBoundsRightOfPanel, toGeoJsonObject } from "@/utils.ts"; +import { fitBoundsBesidePanel, toGeoJsonObject } from "@/utils.ts"; import { useEffect } from "react"; import type { RouteFeatureCollection } from "@/client"; interface RouteBoundsControllerProps { - route: RouteFeatureCollection | undefined; + routes: RouteFeatureCollection | undefined; selectedStepIndex: number | null; } -export const RouteBoundsController = ({ - route, +export const RouteViewportController = ({ + routes, selectedStepIndex, }: RouteBoundsControllerProps) => { const map = useMap(); - const fitRouteBounds = () => { - if (!route) { + const fitDisplayedRouteBounds = () => { + if (!routes) { return; } - const bounds = L.geoJSON(toGeoJsonObject(route)).getBounds(); + const bounds = L.geoJSON(toGeoJsonObject(routes)).getBounds(); if (bounds.isValid()) { // map.fitBounds(bounds, { padding: [30, 30] }); - fitBoundsRightOfPanel(map, bounds); + fitBoundsBesidePanel(map, bounds); } }; useEffect(() => { - fitRouteBounds(); + fitDisplayedRouteBounds(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [route]); + }, [routes]); useEffect(() => { if (selectedStepIndex === null) { - fitRouteBounds(); + fitDisplayedRouteBounds(); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedStepIndex, route]); + }, [selectedStepIndex, routes]); return null; }; diff --git a/frontend/src/SelectedSegment.tsx b/frontend/src/SelectedSegment.tsx index 8a9035d..045f867 100644 --- a/frontend/src/SelectedSegment.tsx +++ b/frontend/src/SelectedSegment.tsx @@ -1,58 +1,65 @@ import { Polyline, useMap } from "react-leaflet"; import { useEffect, useMemo } from "react"; import L from "leaflet"; -import type { LineString, RouteStepResponse } from "@/client"; -import { fitBoundsRightOfPanel } from "@/utils.ts"; +import type { LineString, RouteStepSummary } from "@/client"; +import { fitBoundsBesidePanel } from "@/utils.ts"; interface SelectedSegmentProps { lineString: LineString; - step: RouteStepResponse | null; + selectedStep: RouteStepSummary | null; } -export const SelectedSegment = ({ lineString, step }: SelectedSegmentProps) => { +export const SelectedSegment = ({ + lineString, + selectedStep, +}: SelectedSegmentProps) => { const map = useMap(); - const positions = useMemo(() => { - if (!step) { + const segmentCoordinates = useMemo(() => { + if (!selectedStep) { return null; } - const from = Math.max(0, step.segment_index_from); - const to = Math.min( + const startIndex = Math.max(0, selectedStep.segment_index_from); + const endIndex = Math.min( lineString.coordinates.length - 1, - step.segment_index_to, + selectedStep.segment_index_to, ); - if (to <= from) { + if (endIndex <= startIndex) { return null; } - return lineString.coordinates.slice(from, to + 1); - }, [lineString, step]); + return lineString.coordinates.slice(startIndex, endIndex + 1); + }, [lineString, selectedStep]); useEffect(() => { - if (!positions || positions.length === 0) { + if (!segmentCoordinates || segmentCoordinates.length === 0) { return; } const bounds = L.latLngBounds( - positions.map((pos) => L.latLng(pos[1], pos[0])), + segmentCoordinates.map((coordinate) => + L.latLng(coordinate[1], coordinate[0]), + ), ); if (bounds.isValid()) { // map.fitBounds(bounds, { padding: [40, 40], maxZoom: 18 }); - fitBoundsRightOfPanel(map, bounds); + fitBoundsBesidePanel(map, bounds); } - }, [positions, map]); + }, [segmentCoordinates, map]); - if (!positions) { + if (!segmentCoordinates) { return null; } return ( L.latLng(pos[1], pos[0]))} + positions={segmentCoordinates.map((coordinate) => + L.latLng(coordinate[1], coordinate[0]), + )} pathOptions={{ weight: 6, opacity: 0.8, diff --git a/frontend/src/StatusBar.tsx b/frontend/src/StatusNotice.tsx similarity index 81% rename from frontend/src/StatusBar.tsx rename to frontend/src/StatusNotice.tsx index 6076e7a..5c7ffb7 100644 --- a/frontend/src/StatusBar.tsx +++ b/frontend/src/StatusNotice.tsx @@ -1,11 +1,11 @@ import { Affix, type MantineColor, Paper, Text } from "@mantine/core"; -interface StatusBarProps { +interface StatusNoticeProps { message: string; color: MantineColor; } -export const StatusBar = ({ message, color }: StatusBarProps) => { +export const StatusNotice = ({ message, color }: StatusNoticeProps) => { return ( ; +export const routeScoreLabels = { + snowFreeScore: "Snow-free", + flatScore: "Flat", + scenicScore: "Scenic", +} as const satisfies Record; const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value)); const getFallbackObjectiveCosts = ( distance: number, -): RouteObjectiveCostBreakdown => ({ +): RoutePenaltyBreakdown => ({ distance, snow_penalty: distance, uphill_penalty: distance, scenic_penalty: distance, }); -export const toComfortPercent = (penalty: number, distance: number) => { +export const toPercentScore = (score: number, distance: number) => { if (distance <= 0) { return 0; } - return clamp((1 - penalty / distance) * 100, 0, 100); + return clamp((1 - score / distance) * 100, 0, 100); }; -export const getComfortScores = ( - objectiveCosts: RouteObjectiveCostBreakdown, -): RouteComfortScores => ({ - snowlessComfort: toComfortPercent( +export const getScoreObject = ( + objectiveCosts: RoutePenaltyBreakdown, +): RouteScores => ({ + snowFreeScore: toPercentScore( objectiveCosts.snow_penalty, objectiveCosts.distance, ), - uphillComfort: toComfortPercent( + flatScore: toPercentScore( objectiveCosts.uphill_penalty, objectiveCosts.distance, ), - scenicComfort: toComfortPercent( + scenicScore: toPercentScore( objectiveCosts.scenic_penalty, objectiveCosts.distance, ), }); -export const getRouteComfortScores = ( - route: RouteFeature, -): RouteComfortScores => - getComfortScores( - route.properties.objective_costs ?? +export const getRouteScores = (route: RouteFeature): RouteScores => + getScoreObject( + route.properties.penalty_breakdown ?? getFallbackObjectiveCosts(route.properties.distance), ); -export const getRouteSpeedKmPerHour = ( - transportMode: TransportMode, +export const getTravelSpeedKmh = ( + travelMode: TravelMode, walkingSpeed: number, - bikingSpeed: number, -) => (transportMode === "walk" ? walkingSpeed : bikingSpeed); + cyclingSpeed: number, +) => (travelMode === "walking" ? walkingSpeed : cyclingSpeed); export const estimateRouteDurationMinutes = ( distanceMeters: number, @@ -100,7 +98,7 @@ export const formatDuration = (durationMinutes: number) => { return `${String(hours)} h ${String(minutes).padStart(2, "0")} min`; }; -const buildBucketLabel = (startIndex: number, endIndex: number) => { +const buildStepRangeLabel = (startIndex: number, endIndex: number) => { const start = startIndex + 1; const end = endIndex + 1; @@ -111,10 +109,10 @@ const buildBucketLabel = (startIndex: number, endIndex: number) => { return `${String(start)}-${String(end)}`; }; -export const buildRouteProgressionData = ( - steps: RouteStepResponse[], +export const buildRouteScoreProfile = ( + steps: RouteStepSummary[], maxPoints = 10, -): RouteProgressionDatum[] => { +): RouteScoreProfileDatum[] => { if (steps.length === 0) { return []; } @@ -122,7 +120,7 @@ export const buildRouteProgressionData = ( const bucketSize = steps.length <= maxPoints ? 1 : Math.ceil(steps.length / maxPoints); - const progressionData: RouteProgressionDatum[] = []; + const routeScoreProfile: RouteScoreProfileDatum[] = []; for ( let bucketStart = 0; @@ -132,7 +130,7 @@ export const buildRouteProgressionData = ( const bucketSteps = steps.slice(bucketStart, bucketStart + bucketSize); const bucketObjectiveCosts = bucketSteps.reduce( (totals, step) => { - const objectiveCosts = step.objective_costs; + const objectiveCosts = step.penalty_breakdown; return { distance: totals.distance + objectiveCosts.distance, @@ -146,17 +144,17 @@ export const buildRouteProgressionData = ( snow_penalty: 0, uphill_penalty: 0, scenic_penalty: 0, - } satisfies RouteObjectiveCostBreakdown, + } satisfies RoutePenaltyBreakdown, ); - progressionData.push({ - stepLabel: buildBucketLabel( + routeScoreProfile.push({ + stepLabel: buildStepRangeLabel( bucketStart, Math.min(bucketStart + bucketSize - 1, steps.length - 1), ), - ...getComfortScores(bucketObjectiveCosts), + ...getScoreObject(bucketObjectiveCosts), }); } - return progressionData; + return routeScoreProfile; }; diff --git a/frontend/src/types/global.ts b/frontend/src/types/global.ts index 4fcdd31..50e50f0 100644 --- a/frontend/src/types/global.ts +++ b/frontend/src/types/global.ts @@ -1,2 +1,6 @@ -export type TransportMode = "bike" | "walk"; -export type OverlayAttribute = "snow" | "scenic" | "uphill"; +export type TravelMode = "cycling" | "walking"; + +export type RouteEndpoint = "origin" | "destination"; +export type ActiveRouteEndpoint = RouteEndpoint | null; + +export type MapOverlayKey = "snow" | "scenic" | "hills"; diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts index 5931840..3286463 100644 --- a/frontend/src/utils.ts +++ b/frontend/src/utils.ts @@ -2,7 +2,7 @@ import type { GeoJsonObject, Feature, Polygon, MultiPolygon } from "geojson"; import type { BoundaryFeature, BoundaryFeatureCollection, - LayerFeatureCollection, + OverlayFeatureCollection, RouteFeatureCollection, } from "@/client"; import L from "leaflet"; @@ -12,7 +12,7 @@ export const toGeoJsonObject = ( featureCollection: | RouteFeatureCollection | BoundaryFeatureCollection - | LayerFeatureCollection, + | OverlayFeatureCollection, ): GeoJsonObject => { const copy = { ...featureCollection }; @@ -43,7 +43,7 @@ export const toTurfFeature = ( }; }; -export const fitBoundsRightOfPanel = ( +export const fitBoundsBesidePanel = ( map: L.Map, bounds: L.LatLngBounds, padding = 0, From b49a9815242008ce4f91df326f795ed5d0a24653 Mon Sep 17 00:00:00 2001 From: Martin Kedmenec Date: Fri, 3 Apr 2026 10:41:38 +0200 Subject: [PATCH 3/3] Add walking and cycling nodes/edges as map overlays --- backend/app/graph_layer_service.py | 128 ++++++++++++++++++++++ backend/app/graph_state.py | 42 +++++++ backend/app/main.py | 29 +++++ backend/app/models.py | 47 +++++++- backend/notebooks/test.ipynb | 29 +++-- backend/tests/test_geocoding_and_main.py | 22 ++++ backend/tests/test_graph_layer_service.py | 64 +++++++++++ backend/tests/test_graph_state.py | 19 +++- backend/uv.lock | 42 +++---- frontend/package.json | 2 +- frontend/pnpm-lock.yaml | 18 +-- frontend/src/Map.tsx | 20 ++++ frontend/src/OverlayLayer.tsx | 103 +++++++++++++---- frontend/src/types/global.ts | 5 + frontend/src/utils.ts | 4 +- 15 files changed, 504 insertions(+), 70 deletions(-) create mode 100644 backend/app/graph_layer_service.py create mode 100644 backend/tests/test_graph_layer_service.py diff --git a/backend/app/graph_layer_service.py b/backend/app/graph_layer_service.py new file mode 100644 index 0000000..63f8069 --- /dev/null +++ b/backend/app/graph_layer_service.py @@ -0,0 +1,128 @@ +"""Graph layer filtering and serialization logic.""" + +from typing import TYPE_CHECKING + +from fastapi import HTTPException +from geojson_pydantic import LineString as PydanticLineString +from geojson_pydantic import Point as PydanticPoint +from geojson_pydantic.types import Position, Position2D +from shapely.geometry import LineString as ShapelyLineString +from shapely.geometry import Point as ShapelyPoint +from shapely.geometry import Polygon as ShapelyPolygon + +from app.models import ( + GraphEdgeFeature, + GraphLayerFeatureCollection, + GraphLayerFeatureProperties, + GraphLayerKey, + GraphNodeFeature, +) + +if TYPE_CHECKING: + import geopandas as gpd + from shapely.coords import CoordinateSequence + +from app.layer_service import parse_bounding_box_string + + +def filter_graph_features( + geodataframe: gpd.GeoDataFrame, + *, + bounding_box: str | None, + max_features: int, +) -> gpd.GeoDataFrame: + """Filter graph features by bbox and row limit.""" + filtered_features = geodataframe + + if bounding_box is not None: + min_longitude, min_latitude, max_longitude, max_latitude = ( + parse_bounding_box_string(bounding_box) + ) + bounding_geometry = ShapelyPolygon.from_bounds( + min_longitude, + min_latitude, + max_longitude, + max_latitude, + ) + matching_indices = filtered_features.sindex.query( + bounding_geometry, + predicate="intersects", + ) + filtered_features = filtered_features.iloc[matching_indices] + + if len(filtered_features) > max_features: + filtered_features = filtered_features.head(max_features) + + return filtered_features + + +def build_graph_layer_feature_collection( + geodataframe: gpd.GeoDataFrame, + *, + graph_layer_key: GraphLayerKey, + bounding_box: str | None, + max_features: int, +) -> GraphLayerFeatureCollection: + """Build graph layer FeatureCollection response from node or edge data.""" + filtered_features = filter_graph_features( + geodataframe, + bounding_box=bounding_box, + max_features=max_features, + ) + + graph_features: list[GraphNodeFeature | GraphEdgeFeature] = [] + + for row in filtered_features.itertuples(): + geometry = row.geometry + + if isinstance(geometry, ShapelyPoint): + graph_features.append( + GraphNodeFeature( + type="Feature", + properties=GraphLayerFeatureProperties( + graph_layer_key=graph_layer_key + ), + geometry=PydanticPoint( + type="Point", + coordinates=Position2D( + float(geometry.x), + float(geometry.y), + ), + ), + ) + ) + continue + + if isinstance(geometry, ShapelyLineString): + coordinates: CoordinateSequence = geometry.coords + + if len(coordinates) == 0: + continue + + line_coordinates: list[Position] = [ + Position2D(float(longitude), float(latitude)) + for longitude, latitude in coordinates + ] + graph_features.append( + GraphEdgeFeature( + type="Feature", + properties=GraphLayerFeatureProperties( + graph_layer_key=graph_layer_key + ), + geometry=PydanticLineString( + type="LineString", + coordinates=line_coordinates, + ), + ) + ) + continue + + raise HTTPException( + status_code=500, + detail="Graph layer contains unsupported geometry.", + ) + + return GraphLayerFeatureCollection( + type="FeatureCollection", + features=graph_features, + ) diff --git a/backend/app/graph_state.py b/backend/app/graph_state.py index 4f4170e..f91515f 100644 --- a/backend/app/graph_state.py +++ b/backend/app/graph_state.py @@ -28,6 +28,8 @@ class LoadedGraphState: cycling_graph: MultiDiGraphAny | None = None walking_graph: MultiDiGraphAny | None = None + cycling_nodes: gpd.GeoDataFrame | None = None + walking_nodes: gpd.GeoDataFrame | None = None cycling_edges: gpd.GeoDataFrame | None = None walking_edges: gpd.GeoDataFrame | None = None boundary_geometry: BoundaryGeometry | None = None @@ -36,6 +38,20 @@ class LoadedGraphState: GRAPH_STATE = LoadedGraphState() +def _graph_to_node_geodataframe(graph: MultiDiGraphAny) -> gpd.GeoDataFrame: + """Call OSMnx graph_to_gdfs with the node-only signature.""" + graph_to_gdfs = cast( + "Callable[..., gpd.GeoDataFrame]", + ox.graph_to_gdfs, + ) + + return graph_to_gdfs( + graph, + nodes=True, + edges=False, + ) + + def _graph_to_edge_geodataframe(graph: MultiDiGraphAny) -> gpd.GeoDataFrame: """Call OSMnx graph_to_gdfs with the edge-only signature.""" graph_to_gdfs = cast( @@ -75,6 +91,13 @@ def _geocode_place_to_geodataframe(place_name: str) -> gpd.GeoDataFrame: return geocode_to_gdf(place_name) +def _build_node_geodataframe(graph: MultiDiGraphAny) -> gpd.GeoDataFrame: + """Build node GeoDataFrame from a graph, ensuring WGS84 CRS.""" + node_geodataframe = _graph_to_node_geodataframe(graph) + + return node_geodataframe.set_crs("EPSG:4326", allow_override=True) + + def _build_edge_geodataframe(graph: MultiDiGraphAny) -> gpd.GeoDataFrame: """Build edge GeoDataFrame from a graph, ensuring WGS84 CRS.""" edge_geodataframe = _graph_to_edge_geodataframe(graph) @@ -113,6 +136,8 @@ def load_graph_state( graph_state.cycling_graph = bike_graph graph_state.walking_graph = walk_graph + graph_state.cycling_nodes = _build_node_geodataframe(bike_graph) + graph_state.walking_nodes = _build_node_geodataframe(walk_graph) graph_state.cycling_edges = _build_edge_geodataframe(bike_graph) graph_state.walking_edges = _build_edge_geodataframe(walk_graph) graph_state.boundary_geometry = load_boundary_geometry(place_name) @@ -152,6 +177,23 @@ def get_edge_geodataframe_for_travel_mode( return selected_edges +def get_node_geodataframe_for_travel_mode( + graph_state: LoadedGraphState, + travel_mode: TravelMode, +) -> gpd.GeoDataFrame: + """Return the loaded node GeoDataFrame for the requested transport mode.""" + selected_nodes = ( + graph_state.cycling_nodes + if travel_mode == "cycling" + else graph_state.walking_nodes + ) + + if selected_nodes is None: + raise HTTPException(status_code=500, detail="Node index not loaded.") + + return selected_nodes + + def validate_coordinate_within_boundary( graph_state: LoadedGraphState, longitude: float, diff --git a/backend/app/main.py b/backend/app/main.py index 000fb49..91507f3 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -13,9 +13,11 @@ from starlette.middleware.cors import CORSMiddleware from app.geocoding import reverse_geocode_to_address +from app.graph_layer_service import build_graph_layer_feature_collection from app.graph_state import ( GRAPH_STATE, get_edge_geodataframe_for_travel_mode, + get_node_geodataframe_for_travel_mode, load_graph_state, ) from app.layer_service import build_overlay_feature_collection @@ -26,6 +28,8 @@ BoundaryMeta, BoundaryProperties, CoordinatesRouteRequest, + GraphLayerFeatureCollection, + GraphLayerKey, OverlayFeatureCollection, OverlayKey, ReverseGeocodeResponse, @@ -270,6 +274,31 @@ def list_overlay_features( ) +@app.get("/graph-layers", response_model=GraphLayerFeatureCollection) +def list_graph_layer_features( + graph_layer_key: GraphLayerKey, + bounding_box: str | None = None, + max_features: int = 20_000, +) -> GraphLayerFeatureCollection: + """Get raw OSMnx graph node or edge features for a selected network.""" + is_cycling_layer = graph_layer_key.startswith("cycling_") + is_node_layer = graph_layer_key.endswith("_nodes") + travel_mode: TravelMode = "cycling" if is_cycling_layer else "walking" + + geodataframe = ( + get_node_geodataframe_for_travel_mode(GRAPH_STATE, travel_mode) + if is_node_layer + else get_edge_geodataframe_for_travel_mode(GRAPH_STATE, travel_mode) + ) + + return build_graph_layer_feature_collection( + geodataframe, + graph_layer_key=graph_layer_key, + bounding_box=bounding_box, + max_features=max_features, + ) + + @app.get("/boundaries/current", response_model=BoundaryFeatureCollection) def get_current_boundary() -> BoundaryFeatureCollection: """Get boundary geometry for the loaded routing area.""" diff --git a/backend/app/models.py b/backend/app/models.py index 44cd1f6..94af9e1 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -8,11 +8,23 @@ from geojson_pydantic import Point as PydanticPoint # noqa: TC002 from pydantic import BaseModel, ConfigDict, Field -TravelMode = Literal["walking", "cycling"] -OverlayKey = Literal["snow", "scenic", "hills"] -RouteOptimizationMethod = Literal["shortest", "weighted", "pareto"] +type TravelMode = Literal["walking", "cycling"] +type OverlayKey = Literal["snow", "scenic", "hills"] +type GraphLayerKey = Literal[ + "cycling_nodes", + "cycling_edges", + "walking_nodes", + "walking_edges", +] +type RouteOptimizationMethod = Literal["shortest", "weighted", "pareto"] OVERLAY_KEYS: tuple[OverlayKey, ...] = ("snow", "scenic", "hills") +GRAPH_LAYER_KEYS: tuple[GraphLayerKey, ...] = ( + "cycling_nodes", + "cycling_edges", + "walking_nodes", + "walking_edges", +) class RoutePreferenceWeights(BaseModel): @@ -180,6 +192,35 @@ class OverlayFeatureCollection(BaseModel): features: list[OverlayFeature] +class GraphLayerFeatureProperties(BaseModel): + """Properties attached to a graph layer feature.""" + + graph_layer_key: GraphLayerKey + + +class GraphNodeFeature(BaseModel): + """Graph node feature geometry and properties.""" + + type: Literal["Feature"] + geometry: PydanticPoint + properties: GraphLayerFeatureProperties + + +class GraphEdgeFeature(BaseModel): + """Graph edge feature geometry and properties.""" + + type: Literal["Feature"] + geometry: PydanticLineString + properties: GraphLayerFeatureProperties + + +class GraphLayerFeatureCollection(BaseModel): + """Graph layer FeatureCollection.""" + + type: Literal["FeatureCollection"] + features: list[GraphNodeFeature | GraphEdgeFeature] + + class ReverseGeocodeResponse(BaseModel): """Address response for reverse geocoding.""" diff --git a/backend/notebooks/test.ipynb b/backend/notebooks/test.ipynb index 5bf80fb..7a3577d 100644 --- a/backend/notebooks/test.ipynb +++ b/backend/notebooks/test.ipynb @@ -21,8 +21,12 @@ "id": "89571306487509d9", "metadata": {}, "source": [ - "GRAPH_BIKE = ox.graph.graph_from_place(\n", + "GRAPH_CYCLING = ox.graph.graph_from_place(\n", " \"Copenhagen Municipality, Capital Region of Denmark, Denmark\", network_type=\"bike\"\n", + ")\n", + "\n", + "GRAPH_WALKING = ox.graph.graph_from_place(\n", + " \"Copenhagen Municipality, Capital Region of Denmark, Denmark\", network_type=\"walk\"\n", ")" ], "outputs": [], @@ -33,10 +37,15 @@ "id": "4d11c618dc462b82", "metadata": {}, "source": [ - "GRAPH_BIKE = ox.project_graph(GRAPH_BIKE)\n", + "GRAPH_CYCLING = ox.project_graph(GRAPH_CYCLING)\n", + "GRAPH_WALKING = ox.project_graph(GRAPH_WALKING)\n", "\n", - "GRAPH_BIKE.number_of_nodes()\n", - "GRAPH_BIKE.number_of_edges()" + "display(\"Cycling\")\n", + "display(f\"Nodes: {GRAPH_CYCLING.number_of_nodes()}\")\n", + "display(f\"Edges: {GRAPH_CYCLING.number_of_edges()}\")\n", + "display(\"Walking\")\n", + "display(f\"Nodes: {GRAPH_WALKING.number_of_nodes()}\")\n", + "display(f\"Edges: {GRAPH_WALKING.number_of_edges()}\")" ], "outputs": [], "execution_count": null @@ -45,9 +54,7 @@ "cell_type": "code", "id": "786fa2b4142c599f", "metadata": {}, - "source": [ - "fig, ax = ox.plot.plot_graph(GRAPH_BIKE)" - ], + "source": "fig, ax = ox.plot.plot_graph(GRAPH_CYCLING)", "outputs": [], "execution_count": null }, @@ -55,9 +62,7 @@ "cell_type": "code", "id": "ebb5dc7e6caf7817", "metadata": {}, - "source": [ - "list(GRAPH_BIKE.edges(keys=True, data=True))[34]" - ], + "source": "list(GRAPH_CYCLING.edges(keys=True, data=True))[34]", "outputs": [], "execution_count": null }, @@ -99,8 +104,8 @@ "coordinates = []\n", "\n", "for node_id in path:\n", - " lon = GRAPH_BIKE.nodes[node_id][\"x\"]\n", - " lat = GRAPH_BIKE.nodes[node_id][\"y\"]\n", + " lon = GRAPH_CYCLING.nodes[node_id][\"x\"]\n", + " lat = GRAPH_CYCLING.nodes[node_id][\"y\"]\n", "\n", " coordinates.append((lon, lat))\n", "\n", diff --git a/backend/tests/test_geocoding_and_main.py b/backend/tests/test_geocoding_and_main.py index c56ab24..c059c8a 100644 --- a/backend/tests/test_geocoding_and_main.py +++ b/backend/tests/test_geocoding_and_main.py @@ -16,10 +16,12 @@ compute_route_by_address, compute_route_by_coordinates, get_current_boundary, + list_graph_layer_features, list_overlay_features, reverse_geocode, ) from app.models import ( + GraphLayerFeatureCollection, OverlayFeatureCollection, ReverseGeocodeResponse, RouteCoordinates, @@ -143,6 +145,10 @@ def test_list_layers_and_boundary_endpoints( {"geometry": []}, geometry="geometry", crs="EPSG:4326" ) empty_collection = OverlayFeatureCollection(type="FeatureCollection", features=[]) + empty_graph_collection = GraphLayerFeatureCollection( + type="FeatureCollection", + features=[], + ) def fake_get_edges_for_mode( _state: object, @@ -156,6 +162,12 @@ def fake_build_layer_feature_collection( ) -> OverlayFeatureCollection: return empty_collection + def fake_build_graph_layer_feature_collection( + *_args: object, + **_kwargs: object, + ) -> GraphLayerFeatureCollection: + return empty_graph_collection + monkeypatch.setattr( "app.main.get_edge_geodataframe_for_travel_mode", fake_get_edges_for_mode, @@ -164,10 +176,20 @@ def fake_build_layer_feature_collection( "app.main.build_overlay_feature_collection", fake_build_layer_feature_collection, ) + monkeypatch.setattr( + "app.main.get_node_geodataframe_for_travel_mode", + fake_get_edges_for_mode, + ) + monkeypatch.setattr( + "app.main.build_graph_layer_feature_collection", + fake_build_graph_layer_feature_collection, + ) layers = list_overlay_features("snow", "cycling") + graph_layers = list_graph_layer_features("cycling_nodes") assert layers is empty_collection + assert graph_layers is empty_graph_collection GRAPH_STATE.boundary_geometry = MultiPolygon( [Polygon([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)])] diff --git a/backend/tests/test_graph_layer_service.py b/backend/tests/test_graph_layer_service.py new file mode 100644 index 0000000..a8f0156 --- /dev/null +++ b/backend/tests/test_graph_layer_service.py @@ -0,0 +1,64 @@ +"""Tests for graph layer filtering and feature serialization.""" + +import geopandas as gpd +from shapely.geometry import LineString, Point + +from app.graph_layer_service import ( + build_graph_layer_feature_collection, + filter_graph_features, +) + + +def test_filter_graph_features_applies_bbox_and_limit() -> None: + """Graph feature filtering should keep rows within the bbox and obey the limit.""" + geodataframe = gpd.GeoDataFrame( + { + "geometry": [ + Point(12.0, 55.0), + Point(12.4, 55.4), + Point(13.0, 56.0), + ] + }, + geometry="geometry", + crs="EPSG:4326", + ) + + filtered = filter_graph_features( + geodataframe, + bounding_box="11.9,54.9,12.5,55.5", + max_features=2, + ) + + assert len(filtered) == 2 + + +def test_build_graph_layer_feature_collection_serializes_points_and_lines() -> None: + """Graph layer serialization should preserve point and line geometries.""" + node_geodataframe = gpd.GeoDataFrame( + {"geometry": [Point(12.0, 55.0)]}, + geometry="geometry", + crs="EPSG:4326", + ) + edge_geodataframe = gpd.GeoDataFrame( + {"geometry": [LineString([(12.0, 55.0), (12.1, 55.1)])]}, + geometry="geometry", + crs="EPSG:4326", + ) + + node_collection = build_graph_layer_feature_collection( + node_geodataframe, + graph_layer_key="cycling_nodes", + bounding_box=None, + max_features=10, + ) + edge_collection = build_graph_layer_feature_collection( + edge_geodataframe, + graph_layer_key="walking_edges", + bounding_box=None, + max_features=10, + ) + + assert node_collection.features[0].geometry.type == "Point" + assert node_collection.features[0].properties.graph_layer_key == "cycling_nodes" + assert edge_collection.features[0].geometry.type == "LineString" + assert edge_collection.features[0].properties.graph_layer_key == "walking_edges" diff --git a/backend/tests/test_graph_state.py b/backend/tests/test_graph_state.py index 3171a5d..e4521e9 100644 --- a/backend/tests/test_graph_state.py +++ b/backend/tests/test_graph_state.py @@ -10,14 +10,15 @@ LoadedGraphState, get_edge_geodataframe_for_travel_mode, get_graph_for_travel_mode, + get_node_geodataframe_for_travel_mode, load_boundary_geometry, load_graph_state, validate_coordinate_within_boundary, ) -def test_get_graph_and_edges_for_mode() -> None: - """Graph and edge accessors should return values when state is populated.""" +def test_get_graph_nodes_and_edges_for_mode() -> None: + """Graph, node, and edge accessors should return values when state is populated.""" graph: nx.MultiDiGraph[int] = nx.MultiDiGraph() geodataframe = gpd.GeoDataFrame( {"geometry": []}, geometry="geometry", crs="EPSG:4326" @@ -25,11 +26,14 @@ def test_get_graph_and_edges_for_mode() -> None: state = LoadedGraphState( cycling_graph=graph, walking_graph=graph, + cycling_nodes=geodataframe, + walking_nodes=geodataframe, cycling_edges=geodataframe, walking_edges=geodataframe, ) assert get_graph_for_travel_mode(state, "cycling") is graph + assert get_node_geodataframe_for_travel_mode(state, "cycling").equals(geodataframe) assert get_edge_geodataframe_for_travel_mode(state, "walking").equals(geodataframe) @@ -93,6 +97,11 @@ def fake_build_edge_geodataframe( ) -> gpd.GeoDataFrame: return geodataframe + def fake_build_node_geodataframe( + _graph: nx.MultiDiGraph[int], + ) -> gpd.GeoDataFrame: + return geodataframe + def fake_load_boundary_polygon(_place: str) -> MultiPolygon: return boundary @@ -104,6 +113,10 @@ def fake_load_boundary_polygon(_place: str) -> MultiPolygon: "app.graph_state._build_edge_geodataframe", fake_build_edge_geodataframe, ) + monkeypatch.setattr( + "app.graph_state._build_node_geodataframe", + fake_build_node_geodataframe, + ) monkeypatch.setattr( "app.graph_state.load_boundary_geometry", fake_load_boundary_polygon, @@ -118,6 +131,8 @@ def fake_load_boundary_polygon(_place: str) -> MultiPolygon: assert state.cycling_graph is bike_graph assert state.walking_graph is walk_graph + assert state.cycling_nodes is geodataframe + assert state.walking_nodes is geodataframe assert state.cycling_edges is geodataframe assert state.walking_edges is geodataframe assert state.boundary_geometry is boundary diff --git a/backend/uv.lock b/backend/uv.lock index 7709bcd..00bcd0a 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -2086,27 +2086,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" }, - { url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" }, - { url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" }, - { url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" }, - { url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" }, - { url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" }, - { url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" }, - { url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" }, - { url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" }, - { url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" }, - { url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" }, - { url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" }, - { url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" }, - { url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" }, - { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" }, +version = "0.15.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/97/e9f1ca355108ef7194e38c812ef40ba98c7208f47b13ad78d023caa583da/ruff-0.15.9.tar.gz", hash = "sha256:29cbb1255a9797903f6dde5ba0188c707907ff44a9006eb273b5a17bfa0739a2", size = 4617361, upload-time = "2026-04-02T18:17:20.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/1f/9cdfd0ac4b9d1e5a6cf09bedabdf0b56306ab5e333c85c87281273e7b041/ruff-0.15.9-py3-none-linux_armv6l.whl", hash = "sha256:6efbe303983441c51975c243e26dff328aca11f94b70992f35b093c2e71801e1", size = 10511206, upload-time = "2026-04-02T18:16:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f6/32bfe3e9c136b35f02e489778d94384118bb80fd92c6d92e7ccd97db12ce/ruff-0.15.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4965bac6ac9ea86772f4e23587746f0b7a395eccabb823eb8bfacc3fa06069f7", size = 10923307, upload-time = "2026-04-02T18:17:08.645Z" }, + { url = "https://files.pythonhosted.org/packages/ca/25/de55f52ab5535d12e7aaba1de37a84be6179fb20bddcbe71ec091b4a3243/ruff-0.15.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf05aad70ca5b5a0a4b0e080df3a6b699803916d88f006efd1f5b46302daab8", size = 10316722, upload-time = "2026-04-02T18:16:44.206Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/690d75f3fd6278fe55fff7c9eb429c92d207e14b25d1cae4064a32677029/ruff-0.15.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9439a342adb8725f32f92732e2bafb6d5246bd7a5021101166b223d312e8fc59", size = 10623674, upload-time = "2026-04-02T18:16:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ec/176f6987be248fc5404199255522f57af1b4a5a1b57727e942479fec98ad/ruff-0.15.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5e6faf9d97c8edc43877c3f406f47446fc48c40e1442d58cfcdaba2acea745", size = 10351516, upload-time = "2026-04-02T18:16:57.206Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fc/51cffbd2b3f240accc380171d51446a32aa2ea43a40d4a45ada67368fbd2/ruff-0.15.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b34a9766aeec27a222373d0b055722900fbc0582b24f39661aa96f3fe6ad901", size = 11150202, upload-time = "2026-04-02T18:17:06.452Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d4/25292a6dfc125f6b6528fe6af31f5e996e19bf73ca8e3ce6eb7fa5b95885/ruff-0.15.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89dd695bc72ae76ff484ae54b7e8b0f6b50f49046e198355e44ea656e521fef9", size = 11988891, upload-time = "2026-04-02T18:17:18.575Z" }, + { url = "https://files.pythonhosted.org/packages/13/e1/1eebcb885c10e19f969dcb93d8413dfee8172578709d7ee933640f5e7147/ruff-0.15.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce187224ef1de1bd225bc9a152ac7102a6171107f026e81f317e4257052916d5", size = 11480576, upload-time = "2026-04-02T18:16:52.986Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/a1548ac378a78332a4c3dcf4a134c2475a36d2a22ddfa272acd574140b50/ruff-0.15.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0c7c341f68adb01c488c3b7d4b49aa8ea97409eae6462d860a79cf55f431b6", size = 11254525, upload-time = "2026-04-02T18:17:02.041Z" }, + { url = "https://files.pythonhosted.org/packages/42/aa/4bb3af8e61acd9b1281db2ab77e8b2c3c5e5599bf2a29d4a942f1c62b8d6/ruff-0.15.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:55cc15eee27dc0eebdfcb0d185a6153420efbedc15eb1d38fe5e685657b0f840", size = 11204072, upload-time = "2026-04-02T18:17:13.581Z" }, + { url = "https://files.pythonhosted.org/packages/69/48/d550dc2aa6e423ea0bcc1d0ff0699325ffe8a811e2dba156bd80750b86dc/ruff-0.15.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6537f6eed5cda688c81073d46ffdfb962a5f29ecb6f7e770b2dc920598997ed", size = 10594998, upload-time = "2026-04-02T18:16:46.369Z" }, + { url = "https://files.pythonhosted.org/packages/63/47/321167e17f5344ed5ec6b0aa2cff64efef5f9e985af8f5622cfa6536043f/ruff-0.15.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6d3fcbca7388b066139c523bda744c822258ebdcfbba7d24410c3f454cc9af71", size = 10359769, upload-time = "2026-04-02T18:17:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/67/5e/074f00b9785d1d2c6f8c22a21e023d0c2c1817838cfca4c8243200a1fa87/ruff-0.15.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:058d8e99e1bfe79d8a0def0b481c56059ee6716214f7e425d8e737e412d69677", size = 10850236, upload-time = "2026-04-02T18:16:48.749Z" }, + { url = "https://files.pythonhosted.org/packages/76/37/804c4135a2a2caf042925d30d5f68181bdbd4461fd0d7739da28305df593/ruff-0.15.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8e1ddb11dbd61d5983fa2d7d6370ef3eb210951e443cace19594c01c72abab4c", size = 11358343, upload-time = "2026-04-02T18:16:55.068Z" }, + { url = "https://files.pythonhosted.org/packages/88/3d/1364fcde8656962782aa9ea93c92d98682b1ecec2f184e625a965ad3b4a6/ruff-0.15.9-py3-none-win32.whl", hash = "sha256:bde6ff36eaf72b700f32b7196088970bf8fdb2b917b7accd8c371bfc0fd573ec", size = 10583382, upload-time = "2026-04-02T18:17:04.261Z" }, + { url = "https://files.pythonhosted.org/packages/4c/56/5c7084299bd2cacaa07ae63a91c6f4ba66edc08bf28f356b24f6b717c799/ruff-0.15.9-py3-none-win_amd64.whl", hash = "sha256:45a70921b80e1c10cf0b734ef09421f71b5aa11d27404edc89d7e8a69505e43d", size = 11744969, upload-time = "2026-04-02T18:16:59.611Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/76704c4f312257d6dbaae3c959add2a622f63fcca9d864659ce6d8d97d3d/ruff-0.15.9-py3-none-win_arm64.whl", hash = "sha256:0694e601c028fd97dc5c6ee244675bc241aeefced7ef80cd9c6935a871078f53", size = 11005870, upload-time = "2026-04-02T18:17:15.773Z" }, ] [[package]] diff --git a/frontend/package.json b/frontend/package.json index 6b24575..c8b6cc0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,7 +33,7 @@ "devDependencies": { "@babel/core": "^7.29.0", "@eslint/js": "^9.39.4", - "@hey-api/openapi-ts": "0.94.5", + "@hey-api/openapi-ts": "0.95.0", "@rolldown/plugin-babel": "^0.2.2", "@tanstack/eslint-plugin-query": "^5.96.1", "@tanstack/react-query-devtools": "^5.96.1", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 6674ee5..1755dbb 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -61,8 +61,8 @@ importers: specifier: ^9.39.4 version: 9.39.4 '@hey-api/openapi-ts': - specifier: 0.94.5 - version: 0.94.5(typescript@5.9.3) + specifier: 0.95.0 + version: 0.95.0(typescript@5.9.3) '@rolldown/plugin-babel': specifier: ^0.2.2 version: 0.2.2(@babel/core@7.29.0)(@babel/runtime@7.29.2)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1))(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.8))) @@ -444,15 +444,15 @@ packages: resolution: {integrity: sha512-7atnpUkT8TyUPHYPLk91j/GyaqMuwTEHanLOe50Dlx0EEvNuQqFD52Yjg8x4KU0UFL1mWlyhE+sUE/wAtQ1N2A==} engines: {node: '>=20.19.0'} - '@hey-api/openapi-ts@0.94.5': - resolution: {integrity: sha512-fCR/kIexbDarnt/WGKvjJb4K30JaFzO2F/528kHpyWT7vopPS0JeqtRQMjJg+Gk09N/05nbv1OaFOQXcy0BiVQ==} + '@hey-api/openapi-ts@0.95.0': + resolution: {integrity: sha512-lk5C+WKl5yqEmliQihEyhX/jNcWlAykTSEqkDeKa9xSq5YDAzOFvx7oos8YTqiIzdc4TemtlEaB8Rns7+8A0qg==} engines: {node: '>=20.19.0'} hasBin: true peerDependencies: typescript: '>=5.5.3 || >=6.0.0 || 6.0.1-rc' - '@hey-api/shared@0.2.6': - resolution: {integrity: sha512-ZZrsWbazJcJO688tJVEBeei03B4miPI7OauW+qLMYP/9KL6NadmA5MjqsIIwgfvb0HKMAR7lt4AINKzv0Zwdgw==} + '@hey-api/shared@0.3.0': + resolution: {integrity: sha512-G+4GPojdLEh9bUwRG88teMPM1HdqMm/IsJ38cbnNxhyDu1FkFGwilkA1EqnULCzfTam/ZoZkaLdmAd8xEh4Xsw==} engines: {node: '>=20.19.0'} '@hey-api/spec-types@0.1.0': @@ -2761,11 +2761,11 @@ snapshots: '@types/json-schema': 7.0.15 js-yaml: 4.1.1 - '@hey-api/openapi-ts@0.94.5(typescript@5.9.3)': + '@hey-api/openapi-ts@0.95.0(typescript@5.9.3)': dependencies: '@hey-api/codegen-core': 0.7.4 '@hey-api/json-schema-ref-parser': 1.3.1 - '@hey-api/shared': 0.2.6 + '@hey-api/shared': 0.3.0 '@hey-api/spec-types': 0.1.0 '@hey-api/types': 0.1.4 ansi-colors: 4.1.3 @@ -2776,7 +2776,7 @@ snapshots: transitivePeerDependencies: - magicast - '@hey-api/shared@0.2.6': + '@hey-api/shared@0.3.0': dependencies: '@hey-api/codegen-core': 0.7.4 '@hey-api/json-schema-ref-parser': 1.3.1 diff --git a/frontend/src/Map.tsx b/frontend/src/Map.tsx index 1cae4d6..e01255d 100644 --- a/frontend/src/Map.tsx +++ b/frontend/src/Map.tsx @@ -194,6 +194,26 @@ export const Map = ({ /> )} + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/OverlayLayer.tsx b/frontend/src/OverlayLayer.tsx index a384e6d..95dcda7 100644 --- a/frontend/src/OverlayLayer.tsx +++ b/frontend/src/OverlayLayer.tsx @@ -5,20 +5,36 @@ import { useMap, useMapEvents, } from "react-leaflet"; -import type { MapOverlayKey, TravelMode } from "@/types/global.ts"; -import { useMemo, useState } from "react"; +import type { + GraphLayerKey, + MapOverlayKey, + TravelMode, +} from "@/types/global.ts"; +import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; -import { listOverlayFeaturesLayersGetOptions } from "@/client/@tanstack/react-query.gen.ts"; +import { + listGraphLayerFeaturesGraphLayersGetOptions, + listOverlayFeaturesLayersGetOptions, +} from "@/client/@tanstack/react-query.gen.ts"; import { capitalize, toGeoJsonObject } from "@/utils.ts"; import { Text } from "@mantine/core"; import type { Feature } from "geojson"; import L from "leaflet"; -interface OverlayLayerProps { +interface ThematicOverlayLayerProps { mapOverlayKey: MapOverlayKey; travelMode: TravelMode; + graphLayerKey?: never; +} + +interface GraphOverlayLayerProps { + graphLayerKey: GraphLayerKey; + travelMode?: never; + mapOverlayKey?: never; } +type OverlayLayerProps = ThematicOverlayLayerProps | GraphOverlayLayerProps; + const mapBoundsToBoundingBox = (map: L.Map) => { const bounds = map.getBounds(); @@ -28,8 +44,15 @@ const mapBoundsToBoundingBox = (map: L.Map) => { export const OverlayLayer = ({ mapOverlayKey, travelMode, + graphLayerKey, }: OverlayLayerProps) => { const map = useMap(); + const isGraphLayer = graphLayerKey !== undefined; + const isNodeLayer = isGraphLayer && graphLayerKey.endsWith("_nodes"); + const pointColor = + isGraphLayer && graphLayerKey.startsWith("cycling_") + ? "#2f9e44" + : "#1c7ed6"; const [boundingBox, setBoundingBox] = useState(() => mapBoundsToBoundingBox(map), @@ -44,21 +67,47 @@ export const OverlayLayer = ({ }, }); - const overlayQuery = useQuery({ + const graphLayerQuery = useQuery({ + ...listGraphLayerFeaturesGraphLayersGetOptions({ + query: { + graph_layer_key: graphLayerKey ?? "cycling_edges", + bounding_box: boundingBox, + max_features: 108000, + }, + }), + enabled: isGraphLayer, + placeholderData: (layerFeatureCollection) => layerFeatureCollection, + staleTime: 0, + }); + + const thematicOverlayQuery = useQuery({ ...listOverlayFeaturesLayersGetOptions({ query: { - overlay_key: mapOverlayKey, - travel_mode: travelMode, + overlay_key: mapOverlayKey ?? "snow", + travel_mode: travelMode ?? "walking", bounding_box: boundingBox, minimum_value: 0.01, max_features: 20000, }, }), + enabled: !isGraphLayer, placeholderData: (layerFeatureCollection) => layerFeatureCollection, staleTime: 0, }); - const style = useMemo(() => { + const overlayData = isGraphLayer + ? graphLayerQuery.data + : thematicOverlayQuery.data; + + const style = (feature?: Feature) => { + if (isGraphLayer) { + return { + color: pointColor, + weight: isNodeLayer ? 1 : 2, + opacity: 0.8, + }; + } + let color = "#ffffff"; switch (mapOverlayKey) { @@ -73,30 +122,42 @@ export const OverlayLayer = ({ break; } - return (feature?: Feature) => { - const value = Number(feature?.properties?.value ?? 0); - - const opacity = Math.max(0.1, Math.min(1, value)); + const value = Number(feature?.properties?.value ?? 0); + const opacity = Math.max(0.1, Math.min(1, value)); - return { - color, - weight: 4, - opacity, - }; + return { + color, + weight: 4, + opacity, }; - }, [mapOverlayKey]); + }; - if (overlayQuery.isLoading || !overlayQuery.data) { + if (overlayData === undefined) { return null; } return ( - {capitalize(mapOverlayKey)} area + {capitalize(mapOverlayKey ?? graphLayerKey)} area {/*Intensity: {overlayQuery.data.features[0].properties.value}/1*/} - + + L.circleMarker(latlng, { + radius: 3, + color: pointColor, + weight: 1, + fillColor: pointColor, + fillOpacity: 0.75, + }) + : undefined + } + /> ); }; diff --git a/frontend/src/types/global.ts b/frontend/src/types/global.ts index 50e50f0..df59f5d 100644 --- a/frontend/src/types/global.ts +++ b/frontend/src/types/global.ts @@ -4,3 +4,8 @@ export type RouteEndpoint = "origin" | "destination"; export type ActiveRouteEndpoint = RouteEndpoint | null; export type MapOverlayKey = "snow" | "scenic" | "hills"; +export type GraphLayerKey = + | "cycling_nodes" + | "cycling_edges" + | "walking_nodes" + | "walking_edges"; diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts index 3286463..517549c 100644 --- a/frontend/src/utils.ts +++ b/frontend/src/utils.ts @@ -2,6 +2,7 @@ import type { GeoJsonObject, Feature, Polygon, MultiPolygon } from "geojson"; import type { BoundaryFeature, BoundaryFeatureCollection, + GraphLayerFeatureCollection, OverlayFeatureCollection, RouteFeatureCollection, } from "@/client"; @@ -12,7 +13,8 @@ export const toGeoJsonObject = ( featureCollection: | RouteFeatureCollection | BoundaryFeatureCollection - | OverlayFeatureCollection, + | OverlayFeatureCollection + | GraphLayerFeatureCollection, ): GeoJsonObject => { const copy = { ...featureCollection };