diff --git a/.gitignore b/.gitignore index 62595ef..3a01854 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,8 @@ trip.log # Editor/IDE # .idea/ # .vscode/ + + +# Added by cargo + +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1508c7a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1156 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "as-slice" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45403b49e3954a4b8428a0ac21a4b7afadccf92bfd96273f1a58cd4812496ae0" +dependencies = [ + "generic-array 0.12.4", + "generic-array 0.13.3", + "generic-array 0.14.9", + "stable_deref_trait", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "color-eyre" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f797e67af32588215eaaab8327027ee8e71b9dd0b2b26996aedf20c030fce309" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "geo-types" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24f8647af4005fa11da47cd56252c6ef030be8fa97bdbf355e7dfb6348f0a82c" +dependencies = [ + "approx", + "num-traits", + "rstar 0.10.0", + "rstar 0.11.0", + "rstar 0.12.2", + "rstar 0.8.4", + "rstar 0.9.3", + "serde", +] + +[[package]] +name = "geojson" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e26f3c45b36fccc9cf2805e61d4da6bc4bbd5a3a9589b01afa3a40eff703bd79" +dependencies = [ + "geo-types", + "log", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "hash32" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heapless" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634bd4d29cbf24424d0a4bfcbf80c6960129dc24424752a7d1d1390607023422" +dependencies = [ + "as-slice", + "generic-array 0.14.9", + "hash32 0.1.1", + "stable_deref_trait", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32 0.2.1", + "rustc_version", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32 0.3.1", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6778b0196eefee7df739db78758e5cf9b37412268bfa5650bfeed028aed20d9c" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "owo-colors" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pdqselect" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec91767ecc0a0bbe558ce8c9da33c068066c57ecc8bb8477ef8c1ad3ef77c27" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "instability", + "itertools", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rstar" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a45c0e8804d37e4d97e55c6f258bc9ad9c5ee7b07437009dd152d764949a27c" +dependencies = [ + "heapless 0.6.1", + "num-traits", + "pdqselect", + "serde", + "smallvec", +] + +[[package]] +name = "rstar" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b40f1bfe5acdab44bc63e6699c28b74f75ec43afb59f3eda01e145aff86a25fa" +dependencies = [ + "heapless 0.7.17", + "num-traits", + "serde", + "smallvec", +] + +[[package]] +name = "rstar" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f39465655a1e3d8ae79c6d9e007f4953bfc5d55297602df9dc38f9ae9f1359a" +dependencies = [ + "heapless 0.7.17", + "num-traits", + "serde", + "smallvec", +] + +[[package]] +name = "rstar" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73111312eb7a2287d229f06c00ff35b51ddee180f017ab6dec1f69d62ac098d6" +dependencies = [ + "heapless 0.7.17", + "num-traits", + "serde", + "smallvec", +] + +[[package]] +name = "rstar" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421400d13ccfd26dfa5858199c30a5d76f9c54e0dba7575273025b43c5175dbb" +dependencies = [ + "heapless 0.8.0", + "num-traits", + "serde", + "smallvec", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + +[[package]] +name = "trip" +version = "0.1.0" +dependencies = [ + "color-eyre", + "geojson", + "ratatui", + "serde", + "serde_json", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..266cb6c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "trip" +version = "0.1.0" +edition = "2024" + +[dependencies] +color-eyre = "0.6.5" +geojson = "0.24.2" +ratatui = "0.29.0" +serde = "1.0.228" +serde_json = "1.0.145" diff --git a/api/api.go b/api/api.go deleted file mode 100644 index f839cb2..0000000 --- a/api/api.go +++ /dev/null @@ -1,136 +0,0 @@ -package api - -import ( - "context" - "database/sql" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "os" - "time" - - "github.com/charmbracelet/log" - "github.com/google/go-querystring/query" -) - -const ( - apiV1 = "https://api.transport.nsw.gov.au/v1/tp" -) - -func NewClient(db *sql.DB) *TripClient { - client := &TripClient{ - db: db, - apiKey: os.Getenv("TFNSW_KEY"), - } - - return client -} - -var ( - ErrServerUnavailable = errors.New("server unavailable") - ErrServerInternalError = errors.New("internal error") - ErrServerNotAuthenticated = errors.New("not authenticated") -) - -func (tc *TripClient) fetchData(ctx context.Context, endpoint string, params any) ([]byte, error) { - values, err := query.Values(params) - if err != nil { - log.Error("Error when creating request", "err", err) - return nil, err - } - - url := fmt.Sprintf("%s%s?%s", apiV1, endpoint, values.Encode()) - - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) - if err != nil { - log.Error("Error when creating request", "err", err) - return nil, err - } - req.Header.Add("Authorization", "apikey "+tc.apiKey) - - resp, err := http.DefaultClient.Do(req) - if err != nil { - log.Error("Error when performing request", "err", err) - return nil, err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - log.Error("Error reading body", "err", err) - return nil, err - } - - // The application calling the API has not been authenticated. - if resp.StatusCode == 401 { - log.Errorf("The application calling the API has not been authenticated: %s", string(body)) - return nil, ErrServerNotAuthenticated - } - - if resp.StatusCode == 500 { - log.Errorf("An internal error has occurred: %s", string(body)) - return nil, ErrServerInternalError - } - - if resp.StatusCode == 503 { - log.Errorf("The server is currently unavailable: %s", string(body)) - return nil, ErrServerUnavailable - - } - - if resp.StatusCode != http.StatusOK { - log.Errorf("The server returned an unknown status %s", resp.Status) - return nil, ErrServerInternalError - } - - // success - return body, nil -} - -// gets only current alerts at the current time -// for the current day -func (tc *TripClient) GetCurrentAlerts(ctx context.Context) ([]Alert, error) { - now := time.Now().Format("02-01-2006") - params := alertQuery{ - OutputFormat: "rapidJSON", - FilterPublicationStatus: "current", - Date: now, - } - - data, err := tc.fetchData(ctx, "/add_info", params) - if err != nil { - return nil, err - } - - var parsed alertResponse - json.Unmarshal(data, &parsed) - - return parsed.Infos.Alerts, nil -} - -func (tc *TripClient) TripPlan(ctx context.Context, origin string, destination string) ([]Journey, error) { - now := time.Now().Format("1504") - params := tripQuery{ - OutputFormat: "rapidJSON", - CoordOutputFormat: "EPSG:4326", - DepArrMacro: "dep", // trips departing now - TypeOrigin: "any", - OriginID: origin, - TypeDestination: "any", - DestinationID: destination, - ExcludedMeans: "11", // exclude school buses - Time: now, - } - - data, err := tc.fetchData(ctx, "/trip", params) - if err != nil { - return nil, err - } - - var parsed tripResponse - json.Unmarshal(data, &parsed) - - return parsed.Journeys, err -} diff --git a/api/api_test/api_test.go b/api/api_test/api_test.go deleted file mode 100644 index 7866d79..0000000 --- a/api/api_test/api_test.go +++ /dev/null @@ -1,9 +0,0 @@ -package api_test - -import ( - "testing" - - _ "github.com/mattn/go-sqlite3" -) - -func TestMain(m *testing.M) {} diff --git a/api/api_test/search_test.go b/api/api_test/search_test.go deleted file mode 100644 index 4c7ebb5..0000000 --- a/api/api_test/search_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package api_test - -import ( - "database/sql" - "log" - "testing" - - "github.com/isobelmcrae/trip/api" - "github.com/isobelmcrae/trip/state" - - _ "github.com/mattn/go-sqlite3" -) - -func TestSearchStopSanitiseSearch(t *testing.T) { - verify := func(search string, output string) { - got := api.SanitiseSeach(search) - if got != output { - t.Errorf("%s != %s, got %s", search, output, got) - } - } - - verify(`syd airport`, `syd* airport*`) - verify(`international airport`, `international* airport*`) -} - -func TestSearchStop(t *testing.T) { - db, err := sql.Open("sqlite3", state.DatabasePath) - if err != nil { - log.Fatal(err) - } - defer db.Close() - - tc := api.NewClient(db) - - verify := func(search string, ID ...string) { - stops := tc.FindStop(search) - found := make([]bool, len(ID)) - for _, stop := range stops { - for i, id := range ID { - if stop.ID == id { - found[i] = true - } - } - } - - for i, id := range ID { - if !found[i] { - t.Errorf("stop %s not found in search for %s", id, search) - } - } - } - - verify("syd airport", - "202020", // Sydney Domestic Airport Station - "202030", // Sydney International Airport Station - ) - - verify("int airport", - "202030", // Sydney International Airport Station - ) - - verify("central", - "200060", // Central Station - ) -} diff --git a/api/gtfs.go b/api/gtfs.go deleted file mode 100644 index 67c0eca..0000000 --- a/api/gtfs.go +++ /dev/null @@ -1,150 +0,0 @@ -package api - -import ( - "database/sql" - "fmt" -) - -type GtfsStopTime struct { - TripID string - StopID string - Sequence int - DistanceTraveled float64 -} - -type GtfsTrip struct { - TripID string - RouteID string - ShapeID string -} - -type GtfsShapePoint struct { - ShapeID string - Lat float64 - Lon float64 - Sequence int - DistanceTraveled float64 -} - -/* - -"stopSequence": [ - { - "id": "203114", - "name": "UNSW Gate 2, High St, Randwick", - "disassembledName": "UNSW Gate 2, High St", - "arrivalTimePlanned": "", - "departureTimePlanned": "2025-07-24T08:30:00Z", - "coord": [ - -33.91527, - 151.228025 - ], - "type": "platform" - }, - { - "id": "203346", - "name": "Anzac Pde before Addison St, Kensington", - "disassembledName": "Anzac Pde before Addison St", - "arrivalTimePlanned": "2025-07-24T08:33:00Z", - "departureTimePlanned": "", - "coord": [ - -33.912123, - 151.223384 - ], - "type": "platform" - } -], - -*/ - -/* func (tc *TripClient) FindShapes(originId string, destId string) ([]StopSearchResult, error) { - rows, err := tc.db.Query(` - select distinct t.shape_id - from stop_times as origin_st - join stop_times as dest_st on origin_st.trip_id = dest_st.trip_id - join trips as t on t.trip_id = origin_st.trip_id - where - origin_st.stop_id = ? and - dest_st.stop_id = ? and - origin_st.stop_sequence < dest_st.stop_sequence; - `, originId, destId) - if err != nil { - log.Fatalf("cannot perform search: %v", err) - } - - -} - */ - -func (tc *TripClient) GetJourneyLeg(originStopID, destStopID string) ([][2]float64, error) { - var shapeID string - var startDist, endDist float64 - - journeyDefQuery := ` - SELECT - T.shape_id, - origin_st.shape_dist_traveled AS start_dist, - dest_st.shape_dist_traveled AS end_dist - FROM stop_times AS origin_st - JOIN stop_times AS dest_st ON origin_st.trip_id = dest_st.trip_id - JOIN trips AS T ON T.trip_id = origin_st.trip_id - WHERE - origin_st.stop_id = ? AND - dest_st.stop_id = ? AND - origin_st.stop_sequence < dest_st.stop_sequence AND - T.shape_id IS NOT NULL AND T.shape_id != '' AND - origin_st.shape_dist_traveled IS NOT NULL AND dest_st.shape_dist_traveled IS NOT NULL - LIMIT 1 - ` - - err := tc.db.QueryRow(journeyDefQuery, originStopID, destStopID).Scan(&shapeID, &startDist, &endDist) - - if err != nil { - if err == sql.ErrNoRows { - return nil, fmt.Errorf("no direct path with shape data found between stops %s and %s", originStopID, destStopID) - } - return nil, fmt.Errorf("error querying for journey definition: %w", err) - } - - shapeLegQuery := ` - SELECT - s.shape_pt_lat, - s.shape_pt_lon - FROM shapes AS s - WHERE - s.shape_id = ? AND - s.shape_pt_sequence >= ( - SELECT ss.shape_pt_sequence - FROM shapes AS ss - WHERE ss.shape_id = ? AND ss.shape_dist_traveled <= ? - ORDER BY ss.shape_dist_traveled DESC - LIMIT 1 - ) AND - s.shape_pt_sequence <= ( - SELECT es.shape_pt_sequence - FROM shapes AS es - WHERE es.shape_id = ? AND es.shape_dist_traveled >= ? - ORDER BY es.shape_dist_traveled ASC - LIMIT 1 - ) - ORDER BY s.shape_pt_sequence ASC - ` - - rows, err := tc.db.Query(shapeLegQuery, shapeID, shapeID, startDist, shapeID, endDist) - if err != nil { - return nil, fmt.Errorf("error querying for shape points for leg %s: %w", shapeID, err) - } - defer rows.Close() - - // --- Step 3: Scan the coordinates into the final list. --- - var path [][2]float64 - for rows.Next() { - var lat, lon float64 - if err := rows.Scan(&lat, &lon); err != nil { - return nil, fmt.Errorf("error scanning shape point: %w", err) - } - path = append(path, [2]float64{lat, lon}) - } - - return path, nil -} diff --git a/api/schema.sql b/api/schema.sql deleted file mode 100644 index e78d381..0000000 --- a/api/schema.sql +++ /dev/null @@ -1,41 +0,0 @@ --- sqlite3 api.sqlite -create table if not exists "stop" ( - "id" text not null primary key, - "name" text not null, - "lat" real not null, - "lon" real not null -); - -create virtual table if not exists "stop_fts" using fts5( - id unindexed, - name, - content='', - contentless_unindexed=1 -); - -CREATE TABLE IF NOT EXISTS "stop_times" ( - "trip_id" TEXT NOT NULL, - "stop_id" TEXT NOT NULL, - "stop_sequence" INTEGER NOT NULL, - "shape_dist_traveled" REAL NOT NULL -); - -CREATE TABLE IF NOT EXISTS "trips" ( - "trip_id" TEXT NOT NULL PRIMARY KEY, - "route_id" TEXT NOT NULL, - "shape_id" TEXT -); - -CREATE TABLE IF NOT EXISTS "shapes" ( - "shape_id" TEXT NOT NULL, - "shape_pt_lat" REAL NOT NULL, - "shape_pt_lon" REAL NOT NULL, - "shape_pt_sequence" INTEGER NOT NULL, - "shape_dist_traveled" REAL NOT NULL, - PRIMARY KEY (shape_id, shape_pt_sequence) -); - -CREATE INDEX IF NOT EXISTS "idx_stop_times_trip_id" ON "stop_times" ("trip_id"); -CREATE INDEX IF NOT EXISTS "idx_stop_times_stop_id" ON "stop_times" ("stop_id"); -CREATE INDEX IF NOT EXISTS "idx_trips_shape_id" ON "trips" ("shape_id"); -CREATE INDEX IF NOT EXISTS "idx_shapes_shape_id" ON "shapes" ("shape_id"); diff --git a/api/search.go b/api/search.go deleted file mode 100644 index 3489e09..0000000 --- a/api/search.go +++ /dev/null @@ -1,81 +0,0 @@ -package api - -import ( - "regexp" - - "github.com/charmbracelet/log" -) - -// go build -tags "icu json1 fts5 secure_delete" - -var ( - gReplacementReg = regexp.MustCompile(`[^A-Za-z0-9\s]`) - gWordReg = regexp.MustCompile(`([A-Za-z0-9]+)`) -) - -const ( - SearchStopMaxResults = 25 -) - -// used in tests, useless export -func SanitiseSeach(search string) string { - finalSearch := gReplacementReg.ReplaceAllLiteralString(search, "") - finalSearch = gWordReg.ReplaceAllString(finalSearch, "$1*") - - return finalSearch -} - -type StopSearchResult struct { - ID string - Name string - Lat float64 - Lon float64 -} - -// this should never fail -func (tc *TripClient) FindStop(search string) []StopSearchResult { - // assumed to finish quickly, context unnecessary - rows, err := tc.db.Query(` - select s.id, s.name, s.lat, s.lon - from stop_fts as fts - join stop as s on fts.id = s.id - where fts.name match ? - order by rank - limit ? - `, SanitiseSeach(search), SearchStopMaxResults) - if err != nil { - log.Fatalf("cannot perform search: %v", err) - } - - results := make([]StopSearchResult, 0, SearchStopMaxResults) - - defer rows.Close() - for rows.Next() { - var id string - var name string - var lat float64 - var lon float64 - - err = rows.Scan(&id, &name, &lat, &lon) - if err != nil { - log.Fatalf("cannot scan rows: %v", err) - } - - results = append(results, StopSearchResult{ - ID: id, - Name: name, - Lat: lat, - Lon: lon, - }) - } - - return results -} - -func (tc *TripClient) FindStopFirstOrPanic(search string) StopSearchResult { - results := tc.FindStop(search) - if len(results) == 0 { - log.Fatalf("no results found for search: %s", search) - } - return results[0] -} diff --git a/api/types.go b/api/types.go deleted file mode 100644 index b79dfc4..0000000 --- a/api/types.go +++ /dev/null @@ -1,136 +0,0 @@ -package api - -import "database/sql" - -// TripClientV1, for v1 of the API -type TripClient struct { - db *sql.DB // route searching - apiKey string -} - -// stops -/* type stopQuery struct { - OutputFormat string `url:"outputFormat"` - TypeSf string `url:"type_sf"` - NameSf string `url:"name_sf"` - CoordOutputFormat string `url:"coordOutputFormat"` -} - -type stopResponse struct { - Stops []Stop `json:"locations"` -} */ - -type Stop struct { - ID string `json:"id"` - Name string `json:"disassembledName"` - MatchQuality int `json:"matchQuality"` - Modes []int `json:"modes"` -} - -type alertResponse struct { - Infos struct { - Alerts []Alert `json:"current"` - } `json:"infos"` -} - -type Alert struct { - Content string `json:"content"` - ID string `json:"id"` - Priority string `json:"priority"` - URL string `json:"url"` - URLText string `json:"urlText"` - Type string `json:"type"` - Affected AffectedItems `json:"affected"` -} - -type AffectedItems struct { - Lines []AffectedLines `json:"lines"` - Stops []AffectedStops `json:"stops"` -} - -type AffectedLines struct { - ID string `json:"id"` - Name string `json:"name"` - Number string `json:"number"` -} - -type AffectedStops struct { - ID string `json:"id"` - Name string `json:"name"` - Type string `json:"type"` -} - -type alertQuery struct { - OutputFormat string `url:"outputFormat"` - FilterPublicationStatus string `url:"filterPublicationStatus"` - Date string `url:"filterDateValid"` -} - -type tripQuery struct { - OutputFormat string `url:"outputFormat"` - CoordOutputFormat string `url:"coordOutputFormat"` - DepArrMacro string `url:"depArrMacro"` - TypeOrigin string `url:"type_origin"` - OriginID string `url:"name_origin"` - TypeDestination string `url:"type_destination"` - DestinationID string `url:"name_destination"` - ExcludedMeans string `url:"excludedMeans"` - Time string `url:"itdTime"` -} - -type tripResponse struct { - Journeys []Journey `json:"journeys"` -} - -type Journey struct { - IsAdditional bool `json:"isAdditional"` // indicates it's not the "preferred" journey - Legs []Leg `json:"legs"` - Rating int `json:"rating"` -} - -type Leg struct { - Origin Location `json:"origin"` - Destination Location `json:"destination"` - Duration int `json:"duration"` - Distance int `json:"distance"` - Transportation *Transportation `json:"transportation"` - StopSequence []JourneyStop `json:"stopSequence"` - IsRealtimeControlled bool `json:"isRealtimeControlled"` -} - -type Location struct { - ID string `json:"id"` - Name string `json:"name"` - DisassembledName string `json:"disassembledName"` - ArrivalTimePlanned string `json:"arrivalTimePlanned"` - ArrivalTimeEstimated string `json:"arrivalTimeEstimated"` - DepartureTimePlanned string `json:"departureTimePlanned"` - DepartureTimeEstimated string `json:"departureTimeEstimated"` - Coord []float64 `json:"coord"` - Type string `json:"type"` -} - -type JourneyStop struct { - ID string `json:"id"` - Name string `json:"name"` - DisassembledName string `json:"disassembledName"` - ArrivalTimePlanned string `json:"arrivalTimePlanned"` - DepartureTimePlanned string `json:"departureTimePlanned"` - Coord []float64 `json:"coord"` - Type string `json:"type"` -} - -type Transportation struct { - ID string `json:"id"` - Name string `json:"name"` - Number string `json:"number"` - Description string `json:"description"` - DisassembledName string `json:"disassembledName"` - Destination Destination `json:"destination"` - IconID int `json:"iconId"` -} - -type Destination struct { - ID string `json:"id"` - Name string `json:"name"` -} diff --git a/api_load_data/main.go b/api_load_data/main.go deleted file mode 100644 index 0f532f9..0000000 --- a/api_load_data/main.go +++ /dev/null @@ -1,160 +0,0 @@ -package main - -// go run -tags "icu json1 fts5 secure_delete" ./route/route_load_data ~/Downloads/tt/stops.txt - -import ( - "database/sql" - "encoding/csv" - "fmt" - "io" - "log" - "os" - "strconv" - - "github.com/isobelmcrae/trip/api" - _ "github.com/mattn/go-sqlite3" -) - -func parseCSV[T any](filePath string, parser func([]string, map[string]int) (T, error)) ([]T, error) { - file, err := os.Open(filePath) - if err != nil { - return nil, fmt.Errorf("error opening %s: %w", filePath, err) - } - defer file.Close() - - // Skip UTF-8 BOM if present - bom := make([]byte, 3) - file.Read(bom) - if string(bom) != "\xef\xbb\xbf" { - file.Seek(0, 0) - } - - reader := csv.NewReader(file) - reader.TrimLeadingSpace = true - header, err := reader.Read() - if err != nil { - return nil, fmt.Errorf("error reading header from %s: %w", filePath, err) - } - - colIndex := make(map[string]int) - for i, h := range header { - colIndex[h] = i - } - - var results []T - for { - record, err := reader.Read() - if err == io.EOF { - break - } - if err != nil { - return nil, fmt.Errorf("error reading record from %s: %w", filePath, err) - } - item, err := parser(record, colIndex) - if err == nil { - results = append(results, item) - } - } - return results, nil -} - -func main() { - // go run api_load_schema ./app.sqlite ./stops.txt - databasePath := os.Args[1] - gtfsPath := os.Args[2] - - stops, _ := parseCSV(gtfsPath + "/stops.txt", func(r []string, c map[string]int) (api.StopSearchResult, error) { - if r[c["parent_station"]] != "" { - return api.StopSearchResult{}, fmt.Errorf("is child station") - } - lat, _ := strconv.ParseFloat(r[c["stop_lat"]], 64) - lon, _ := strconv.ParseFloat(r[c["stop_lon"]], 64) - return api.StopSearchResult{ID: r[c["stop_id"]], Name: r[c["stop_name"]], Lat: lat, Lon: lon}, nil - }) - - stopTimes, _ := parseCSV(gtfsPath + "/stop_times.txt", func(r []string, c map[string]int) (api.GtfsStopTime, error) { - seq, _ := strconv.Atoi(r[c["stop_sequence"]]) - dist, _ := strconv.ParseFloat(r[c["shape_dist_traveled"]], 64) - return api.GtfsStopTime{TripID: r[c["trip_id"]], StopID: r[c["stop_id"]], Sequence: seq, DistanceTraveled: dist }, nil - }) - - trips, _ := parseCSV(gtfsPath + "/trips.txt", func(r []string, c map[string]int) (api.GtfsTrip, error) { - return api.GtfsTrip{TripID: r[c["trip_id"]], RouteID: r[c["route_id"]], ShapeID: r[c["shape_id"]]}, nil - }) - - shapePoints, _ := parseCSV(gtfsPath + "/shapes.txt", func(r []string, c map[string]int) (api.GtfsShapePoint, error) { - lat, _ := strconv.ParseFloat(r[c["shape_pt_lat"]], 64) - lon, _ := strconv.ParseFloat(r[c["shape_pt_lon"]], 64) - seq, _ := strconv.Atoi(r[c["shape_pt_sequence"]]) - dist, _ := strconv.ParseFloat(r[c["shape_dist_traveled"]], 64) - return api.GtfsShapePoint{ShapeID: r[c["shape_id"]], Lat: lat, Lon: lon, Sequence: seq, DistanceTraveled: dist}, nil - }) - - db, err := sql.Open("sqlite3", databasePath) - if err != nil { - log.Fatal(err) - } - defer db.Close() - - // Use a transaction for massive speed improvement - tx, err := db.Begin() - if err != nil { - log.Fatal(err) - } - - log.Println("Inserting stops...") - stmt, err := tx.Prepare("INSERT INTO stop(id, name, lat, lon) VALUES(?, ?, ?, ?)") - if err != nil { - log.Fatal(err) - } - for _, s := range stops { - stmt.Exec(s.ID, s.Name, s.Lat, s.Lon) - } - stmt.Close() - - log.Println("Inserting stop_times...") - stmt, err = tx.Prepare("INSERT INTO stop_times(trip_id, stop_id, stop_sequence, shape_dist_traveled) VALUES(?, ?, ?, ?)") - if err != nil { - log.Fatal(err) - } - for _, st := range stopTimes { - stmt.Exec(st.TripID, st.StopID, st.Sequence, st.DistanceTraveled) - } - stmt.Close() - - log.Println("Inserting trips...") - stmt, err = tx.Prepare("INSERT INTO trips(trip_id, route_id, shape_id) VALUES(?, ?, ?)") - if err != nil { - log.Fatal(err) - } - for _, t := range trips { - stmt.Exec(t.TripID, t.RouteID, t.ShapeID) - } - stmt.Close() - - log.Println("Inserting shapes...") - stmt, err = tx.Prepare("INSERT INTO shapes(shape_id, shape_pt_lat, shape_pt_lon, shape_pt_sequence, shape_dist_traveled) VALUES(?, ?, ?, ?, ?)") - if err != nil { - log.Fatal(err) - } - for _, sp := range shapePoints { - stmt.Exec(sp.ShapeID, sp.Lat, sp.Lon, sp.Sequence, sp.DistanceTraveled) - } - stmt.Close() - - log.Println("Committing transaction...") - if err := tx.Commit(); err != nil { - log.Fatal(err) - } - log.Println("Database load complete.") - - // Rebuild FTS index - log.Println("Rebuilding FTS index...") - db.Exec(`INSERT INTO stop_fts (id, name) SELECT id, name FROM stop;`) - db.Exec(`INSERT INTO stop_fts(stop_fts) VALUES('optimize');`) - - log.Println("VACUUM + ANALYZE...") - db.Exec(`VACUUM;`) - db.Exec(`ANALYZE;`) - log.Println("FTS index complete.") -} diff --git a/go.mod b/go.mod deleted file mode 100644 index c31594b..0000000 --- a/go.mod +++ /dev/null @@ -1,57 +0,0 @@ -module github.com/isobelmcrae/trip - -go 1.24.5 - -require ( - github.com/76creates/stickers v1.4.1 - github.com/charmbracelet/bubbles v0.21.0 - github.com/charmbracelet/bubbletea v1.3.6 - github.com/charmbracelet/lipgloss v1.1.0 - github.com/charmbracelet/log v0.4.2 - github.com/charmbracelet/ssh v0.0.0-20250429213052-383d50896132 - github.com/charmbracelet/wish v1.4.7 - github.com/flywave/go-earcut v0.0.0-20210712015426-7084f78cceb3 - github.com/google/go-querystring v1.1.0 - github.com/joho/godotenv v1.5.1 - github.com/mattn/go-runewidth v0.0.16 - github.com/mattn/go-sqlite3 v1.14.28 - github.com/paulmach/orb v0.11.1 - github.com/tidwall/rtree v1.10.0 -) - -require ( - github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect - github.com/atotto/clipboard v0.1.4 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect - github.com/charmbracelet/keygen v0.5.3 // indirect - github.com/charmbracelet/x/ansi v0.9.3 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect - github.com/charmbracelet/x/conpty v0.1.0 // indirect - github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 // indirect - github.com/charmbracelet/x/input v0.3.4 // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect - github.com/charmbracelet/x/termios v0.1.0 // indirect - github.com/charmbracelet/x/windows v0.2.0 // indirect - github.com/creack/pty v1.1.21 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/go-logfmt/logfmt v0.6.0 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect - github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/termenv v0.16.0 // indirect - github.com/paulmach/protoscan v0.2.1 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/sahilm/fuzzy v0.1.1 // indirect - github.com/tidwall/geoindex v1.7.0 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - go.mongodb.org/mongo-driver v1.11.4 // indirect - golang.org/x/crypto v0.37.0 // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.26.0 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 61311fc..0000000 --- a/go.sum +++ /dev/null @@ -1,184 +0,0 @@ -github.com/76creates/stickers v1.4.1 h1:cd9qM1+FuM7TFStTRxEMEWNpyCY5CIJ05xvKdA+fwk4= -github.com/76creates/stickers v1.4.1/go.mod h1:S0ii0IRGMJx5n5zGpesai8oX0DWY3X5PDI3OUErgF38= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= -github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= -github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= -github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= -github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= -github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= -github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= -github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= -github.com/charmbracelet/keygen v0.5.3 h1:2MSDC62OUbDy6VmjIE2jM24LuXUvKywLCmaJDmr/Z/4= -github.com/charmbracelet/keygen v0.5.3/go.mod h1:TcpNoMAO5GSmhx3SgcEMqCrtn8BahKhB8AlwnLjRUpk= -github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= -github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig= -github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw= -github.com/charmbracelet/ssh v0.0.0-20250429213052-383d50896132 h1:ILyX/vWS/SeHfyuMFAsaV95jEKrwivNUkA8tE7JEXs4= -github.com/charmbracelet/ssh v0.0.0-20250429213052-383d50896132/go.mod h1:R9cISUs5kAH4Cq/rguNbSwcR+slE5Dfm8FEs//uoIGE= -github.com/charmbracelet/wish v1.4.7 h1:O+jdLac3s6GaqkOHHSwezejNK04vl6VjO1A+hl8J8Yc= -github.com/charmbracelet/wish v1.4.7/go.mod h1:OBZ8vC62JC5cvbxJLh+bIWtG7Ctmct+ewziuUWK+G14= -github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= -github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= -github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= -github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= -github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= -github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= -github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= -github.com/charmbracelet/x/input v0.3.4 h1:Mujmnv/4DaitU0p+kIsrlfZl/UlmeLKw1wAP3e1fMN0= -github.com/charmbracelet/x/input v0.3.4/go.mod h1:JI8RcvdZWQIhn09VzeK3hdp4lTz7+yhiEdpEQtZN+2c= -github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= -github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= -github.com/charmbracelet/x/termios v0.1.0 h1:y4rjAHeFksBAfGbkRDmVinMg7x7DELIGAFbdNvxg97k= -github.com/charmbracelet/x/termios v0.1.0/go.mod h1:H/EVv/KRnrYjz+fCYa9bsKdqF3S8ouDK0AZEbG7r+/U= -github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw= -github.com/charmbracelet/x/windows v0.2.0/go.mod h1:ZibNFR49ZFqCXgP76sYanisxRyC+EYrBE7TTknD8s1s= -github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= -github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/flywave/go-earcut v0.0.0-20210712015426-7084f78cceb3 h1:ySHLqVmIxR+3M48bEb5YT17O3abCmcM3S9QgdbSaxag= -github.com/flywave/go-earcut v0.0.0-20210712015426-7084f78cceb3/go.mod h1:rkDc3uj7QKZmizk9QXYN92ZjULyvsCCNILinl3kEWws= -github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= -github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= -github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= -github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= -github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= -github.com/paulmach/protoscan v0.2.1 h1:rM0FpcTjUMvPUNk2BhPJrreDKetq43ChnL+x1sRg8O8= -github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= -github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tidwall/cities v0.1.0 h1:CVNkmMf7NEC9Bvokf5GoSsArHCKRMTgLuubRTHnH0mE= -github.com/tidwall/cities v0.1.0/go.mod h1:lV/HDp2gCcRcHJWqgt6Di54GiDrTZwh1aG2ZUPNbqa4= -github.com/tidwall/geoindex v1.7.0 h1:jtk41sfgwIt8MEDyC3xyKSj75iXXf6rjReJGDNPtR5o= -github.com/tidwall/geoindex v1.7.0/go.mod h1:rvVVNEFfkJVWGUdEfU8QaoOg/9zFX0h9ofWzA60mz1I= -github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8= -github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8= -github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= -github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tidwall/rtree v1.10.0 h1:+EcI8fboEaW1L3/9oW/6AMoQ8HiEIHyR7bQOGnmz4Mg= -github.com/tidwall/rtree v1.10.0/go.mod h1:iDJQ9NBRtbfKkzZu02za+mIlaP+bjYPnunbSNidpbCQ= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= -github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.mongodb.org/mongo-driver v1.11.4 h1:4ayjakA013OdpGyL2K3ZqylTac/rMjrJOMZ1EHizXas= -go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go deleted file mode 100644 index abc32ce..0000000 --- a/main.go +++ /dev/null @@ -1,114 +0,0 @@ -package main - -import ( - "context" - "errors" - "flag" - "os" - "os/signal" - "syscall" - "time" - - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/log" - "github.com/charmbracelet/ssh" - "github.com/charmbracelet/wish" - "github.com/charmbracelet/wish/activeterm" - wishbtea "github.com/charmbracelet/wish/bubbletea" - "github.com/charmbracelet/wish/logging" - ui "github.com/isobelmcrae/trip/ui" - "github.com/joho/godotenv" -) - -const ( - defaultSSHAddr = "0.0.0.0:23234" -) - -func main() { - err := godotenv.Load() - if err != nil { - log.Error("Error loading .env") - } - sshMode := flag.Bool("ssh", false, "run as SSH‐served TUI") - sshAddr := flag.String("addr", defaultSSHAddr, "SSH listen address (host:port)") - flag.Parse() - - // configure logging to file - f, err := os.OpenFile("trip.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - panic(err) - } - log.SetOutput(f) - log.SetLevel(log.DebugLevel) - log.SetReportCaller(true) - - // Force sydney timezone for now - // FIXME: repair automatic timezone detection in the future - time.Local, _ = time.LoadLocation("Australia/Sydney") - - if *sshMode { - runSSH(*sshAddr) - } else { - runLocal() - } -} - -// runLocal starts your TUI in the current terminal -func runLocal() { - m := ui.InitialiseRootModel() - p := tea.NewProgram(m, tea.WithAltScreen()) - if _, err := p.Run(); err != nil { - log.Fatal("TUI error:", err) - } -} - -// runSSH spins up a Wish SSH server that serves your TUI over SSH -func runSSH(addr string) { - server, err := wish.NewServer( - wish.WithAddress(addr), - wish.WithHostKeyPath(".ssh/id_ed25519"), - wish.WithMiddleware( - wishbtea.Middleware(sshHandler), - activeterm.Middleware(), - logging.Middleware(), - ), - ) - if err != nil { - log.Fatal("could not start SSH server", "err", err) - } - - // graceful shutdown - sig := make(chan os.Signal, 1) - signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) - go func() { - <-sig - log.Info("shutting down SSH server…") - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := server.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) { - log.Error("shutdown error", "err", err) - } - os.Exit(0) - }() - - log.Info("SSH server listening", "addr", addr) - if err := server.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) { - log.Fatal("server error", "err", err) - } -} - -// sshHandler wires each incoming SSH session to your Bubble Tea model -func sshHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) { - _, winCh, _ := s.Pty() - // pass session context so you can cancel on disconnect, etc. - m := ui.InitialiseRootModel() - - // forward window‐resize events - go func() { - for win := range winCh { - m.Update(tea.WindowSizeMsg{Width: win.Width, Height: win.Height}) - } - }() - - return m, []tea.ProgramOption{tea.WithAltScreen()} -} diff --git a/rendermaps/cache.go b/rendermaps/cache.go deleted file mode 100644 index 83f0346..0000000 --- a/rendermaps/cache.go +++ /dev/null @@ -1,34 +0,0 @@ -package rendermaps - -import ( - "os" - "path/filepath" -) - -var ( - localCache = createCache() -) - -func createCache() string { - cache := filepath.Join(os.Getenv("HOME"), ".cache", "trip") - if err := os.MkdirAll(cache, 0755); err != nil { - panic(err) - } - return cache -} - -func cacheInsertKey(key string, value []byte) { - cacheFile := filepath.Join(localCache, key) - if err := os.WriteFile(cacheFile, value, 0644); err != nil { - panic(err) - } -} - -func cacheGetKey(key string) ([]byte, error) { - cacheFile := filepath.Join(localCache, key) - data, err := os.ReadFile(cacheFile) - if err != nil { - return nil, err - } - return data, nil -} diff --git a/rendermaps/canvas.go b/rendermaps/canvas.go deleted file mode 100644 index 3d0104b..0000000 --- a/rendermaps/canvas.go +++ /dev/null @@ -1,361 +0,0 @@ -package rendermaps - -import ( - "sort" - "strings" - - "github.com/flywave/go-earcut" - "github.com/mattn/go-runewidth" - "github.com/paulmach/orb" - "github.com/paulmach/orb/simplify" -) - -func renderTileLayer(canvas *Canvas, lb *LabelBuffer, tile *Tile, pos orb.Point, tileSize, zoom float64, layerName string) { - if tile.Extent == 0 { - return // Skip empty/invalid tiles - } - scale := float64(tile.Extent) / tileSize - tile.Rtree.Search( - [2]float64{-pos.X() * scale, -pos.Y() * scale}, - [2]float64{(float64(canvas.width) - pos.X()) * scale, (float64(canvas.height) - pos.Y()) * scale}, - func(_, _ [2]float64, data interface{}) bool { - feature := data.(*StyledFeature) - if feature.Style.SourceLayer == layerName { - drawFeature(canvas, lb, feature, pos, scale, zoom) - } - return true - }, - ) -} - -func drawFeature(canvas *Canvas, lb *LabelBuffer, feature *StyledFeature, pos orb.Point, scale, zoom float64) { - if (feature.Style.MinZoom != 0 && zoom < feature.Style.MinZoom) || (feature.Style.MaxZoom != 0 && zoom > feature.Style.MaxZoom) { - return - } - - transform := func(p orb.Point) orb.Point { - return orb.Point{pos.X() + p.X()/scale, pos.Y() + p.Y()/scale} - } - - switch feature.Style.Type { - case "fill": - if polygon, ok := feature.Geometry.(orb.Polygon); ok { - var rings []orb.Ring - for _, ring := range polygon { - transformedRing := make(orb.Ring, len(ring)) - for i, p := range ring { - transformedRing[i] = transform(p) - } - rings = append(rings, transformedRing) - } - canvas.Polygon(rings, feature.Color) - } - case "line": - if ls, ok := feature.Geometry.(orb.LineString); ok { - points := make(orb.LineString, len(ls)) - for i, p := range ls { - points[i] = transform(p) - } - simplifier := simplify.DouglasPeucker(0.5) - simplifiedGeom := simplifier.Simplify(points) - - if simplifiedLine, ok := simplifiedGeom.(orb.LineString); ok { - canvas.Polyline(simplifiedLine, feature.Color) - } - // simplified, _ := planar.Simplify(nil, 0.5, points...) - // canvas.Polyline(simplified, feature.Color) - } - case "symbol": - label := feature.Label - if label == "" { - label = POIMarker - } - if p, ok := feature.Geometry.(orb.Point); ok { - tp := transform(p) - charX, charY := int(tp.X()/2), int(tp.Y()/4) - if lb.WriteIfPossible(label, charX, charY) { - canvas.Text(label, int(tp.X()), int(tp.Y()), feature.Color) - } - } - } -} - -type Canvas struct { - width, height int - pixelBuffer []byte - charBuffer map[int]rune - colorBuffer map[int]string - brailleMap [4][2]byte -} - -func NewCanvas(width, height int) *Canvas { - size := (width / 2) * (height / 4) - return &Canvas{ - width: width, height: height, pixelBuffer: make([]byte, size), - charBuffer: make(map[int]rune), colorBuffer: make(map[int]string), - brailleMap: [4][2]byte{{0x01, 0x08}, {0x02, 0x10}, {0x04, 0x20}, {0x40, 0x80}}, - } -} - -func (c *Canvas) project(x, y int) (int, bool) { - if x < 0 || x >= c.width || y < 0 || y >= c.height { - return 0, false - } - return (x / 2) + (c.width/2)*(y/4), true -} - -func (c *Canvas) SetPixel(x, y int, color string) { - if idx, ok := c.project(x, y); ok { - c.pixelBuffer[idx] |= c.brailleMap[y%4][x%2] - if _, exists := c.colorBuffer[idx]; !exists { - c.colorBuffer[idx] = color - } - } -} - -func (c *Canvas) setPixelSplat(x, y int, color string) { - if idx, ok := c.project(x, y); ok { - // c.pixelBuffer[idx] |= 0xff // splat! - // c.pixelBuffer[idx] |= c.brailleMap[y%4][x%2] - // c.charBuffer[idx] = '•' - c.charBuffer[idx] = '⬤' - c.colorBuffer[idx] = color - } -} - -func (c *Canvas) Text(text string, x, y int, color string) { - // Center text - x -= (runewidth.StringWidth(text) / 2) * 2 - for i, r := range text { - if idx, ok := c.project(x+i*2, y); ok { - c.charBuffer[idx] = r - c.colorBuffer[idx] = color - } - } -} - -func (c *Canvas) Frame() string { - var sb strings.Builder - termReset := "\x1B[0m" - currentColor := "" - for y := 0; y < c.height/4; y++ { - for x := 0; x < c.width/2; x++ { - idx := x + y*(c.width/2) - colorCode := c.colorBuffer[idx] - if colorCode != currentColor { - sb.WriteString(termReset) - if colorCode != "" { - sb.WriteString(colorCode) - } - currentColor = colorCode - } - if char, ok := c.charBuffer[idx]; ok { - sb.WriteRune(char) - } else if pixelVal := c.pixelBuffer[idx]; pixelVal > 0 { - sb.WriteRune(rune(0x2800 + int(pixelVal))) - } else { - sb.WriteRune(' ') - } - } - sb.WriteString(termReset) - currentColor = "" - if y < c.height/4-1 { - sb.WriteRune('\n') - } - } - return sb.String() -} - -func (c *Canvas) Polyline(points []orb.Point, color string) { - for i := 0; i < len(points)-1; i++ { - c.line(points[i], points[i+1], color) - } -} - -var redColour = ("#ff0000") - -// to be used after everything is rendered -func (c *Canvas) SplatLineGeo( - originLat, originLon, destLat, destLon float64, - mapCenterLat, mapCenterLon, - mapZoom float64, colour string, -) { - canvasP1 := geoToPixel( - originLat, originLon, - mapCenterLat, mapCenterLon, mapZoom, - c.width, c.height, - ) - - canvasP2 := geoToPixel( - destLat, destLon, - mapCenterLat, mapCenterLon, mapZoom, - c.width, c.height, - ) - - c.line(canvasP1, canvasP2, hexToANSI(colour), true) -} - -func (c *Canvas) line(p1, p2 orb.Point, color string, impl ...bool) { - var setPixel bool - if len(impl) > 0 && impl[0] { - setPixel = true - } else { - setPixel = false - } - - x0, y0, x1, y1 := int(p1.X()), int(p1.Y()), int(p2.X()), int(p2.Y()) - - dx := x1 - x0 - if dx < 0 { - dx = -dx - } - sx := -1 - if x0 < x1 { - sx = 1 - } - - dy := y1 - y0 - if dy < 0 { - dy = -dy - } - dy = -dy // This is correct for the algorithm's error term calculation - - sy := -1 - if y0 < y1 { - sy = 1 - } - - err := dx + dy - - for { - if setPixel { - c.setPixelSplat(x0, y0, color) - } else { - c.SetPixel(x0, y0, color) - } - if x0 == x1 && y0 == y1 { - break - } - e2 := 2 * err - if e2 >= dy { - err += dy - x0 += sx - } - if e2 <= dx { - err += dx - y0 += sy - } - } -} - -func (c *Canvas) Polygon(rings []orb.Ring, color string) { - if len(rings) == 0 { - return - } - var vertices []float64 - var holes []int - for i, ring := range rings { - if i > 0 { - holes = append(holes, len(vertices)/2) - } - for _, p := range ring { - vertices = append(vertices, p.X(), p.Y()) - } - } - triangles, err := earcut.Earcut(vertices, holes, 2) - if err != nil { - panic("unreachable") - } - for i := 0; i < len(triangles); i += 3 { - p1Idx, p2Idx, p3Idx := triangles[i]*2, triangles[i+1]*2, triangles[i+2]*2 - p1 := orb.Point{vertices[p1Idx], vertices[p1Idx+1]} - p2 := orb.Point{vertices[p2Idx], vertices[p2Idx+1]} - p3 := orb.Point{vertices[p3Idx], vertices[p3Idx+1]} - c.filledTriangle(p1, p2, p3, color) - } -} - -func bresenham(p1, p2 orb.Point) []orb.Point { - var points []orb.Point - x0, y0, x1, y1 := int(p1.X()), int(p1.Y()), int(p2.X()), int(p2.Y()) - - dx := x1 - x0 - if dx < 0 { - dx = -dx - } - sx := -1 - if x0 < x1 { - sx = 1 - } - - dy := y1 - y0 - if dy < 0 { - dy = -dy - } - dy = -dy // This is part of the algorithm for all octants - sy := -1 - if y0 < y1 { - sy = 1 - } - - err := dx + dy - for { - points = append(points, orb.Point{float64(x0), float64(y0)}) - if x0 == x1 && y0 == y1 { - break - } - e2 := 2 * err - if e2 >= dy { - err += dy - x0 += sx - } - if e2 <= dx { - err += dx - y0 += sy - } - } - return points -} - -func (c *Canvas) filledTriangle(p1, p2, p3 orb.Point, color string) { - // Get all points on the triangle's edges - edge1 := bresenham(p1, p2) - edge2 := bresenham(p2, p3) - edge3 := bresenham(p3, p1) - - allPoints := append(edge1, edge2...) - allPoints = append(allPoints, edge3...) - - // Sort points primarily by Y, then by X - sort.Slice(allPoints, func(i, j int) bool { - if allPoints[i].Y() == allPoints[j].Y() { - return allPoints[i].X() < allPoints[j].X() - } - return allPoints[i].Y() < allPoints[j].Y() - }) - - // Fill between the points on each scanline - if len(allPoints) == 0 { - return - } - - for i := 0; i < len(allPoints)-1; { - pStart := allPoints[i] - pEnd := pStart - - // Find the last point on the same scanline - j := i - for j < len(allPoints) && allPoints[j].Y() == pStart.Y() { - pEnd = allPoints[j] - j++ - } - - // Draw the horizontal line - y := int(pStart.Y()) - for x := int(pStart.X()); x <= int(pEnd.X()); x++ { - c.SetPixel(x, y, color) - } - - i = j // Move to the next scanline - } -} diff --git a/rendermaps/example/main.go b/rendermaps/example/main.go deleted file mode 100644 index b964032..0000000 --- a/rendermaps/example/main.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "fmt" - "time" - - "github.com/isobelmcrae/trip/rendermaps" -) - -func main() { - t0 := time.Now() - - str := rendermaps.RenderMapOneshot(120, 40, -33.88402424, 151.20620308, 14) - - println(str) - - t1 := time.Now() - str = rendermaps.RenderMapOneshot(120, 40, -33.88402424, 151.20620308, 14) - t2 := time.Now() - - fmt.Println(str) - fmt.Println("Rendered map (cold) in", t1.Sub(t0)) - fmt.Println("Rendered map (hot) in", t2.Sub(t1)) - - centerLat, centerLon, zoom := rendermaps.FocusOn( - -33.884179, 151.207215, // central - -33.861351, 151.210295, // circular quay - 120, 40, - ) - - renderer := rendermaps.RenderMap(120, 40, centerLat, centerLon, zoom) - renderer.Draw([]string{"landuse", "water", "building", "road", "admin"}) - - renderer.Canvas.SplatLineGeo( - -33.884179, 151.207215, // central - -33.861351, 151.210295, // circular quay - centerLat, centerLon, - zoom, "#ff0000", // red line - ) - - renderer.Draw([]string{"place_label", "poi_label"}) - - str = renderer.Frame() - fmt.Println(str) - - /* if err != nil { - panic(err) - } - - canvas.RedLineGeo( - -33.884179, 151.207215, // central - -33.861351, 151.210295, // circular quay - centerLat, centerLon, - zoom, - ) - - str = canvas.Frame() - fmt.Println(str) */ - - /* for i := 0; i < 360; i++ { - lat := -33.88402424 + 0.01 * float64(i) * 0.017453292519943295 // 0.01 degrees in radians - lon := 151.20620308 + 0.01 * float64(i) * 0.017453292519943295 // 0.01 degrees in radians - str, err := rendermaps.RenderMap(120, 40, lat, lon, 14) - if err != nil { - panic(err) - } - os.Stdout.WriteString(str + "\n") - os.Stdout.Sync() - - time.Sleep(100 * time.Millisecond) - } */ -} diff --git a/rendermaps/maps.go b/rendermaps/maps.go deleted file mode 100644 index 3c4df0e..0000000 --- a/rendermaps/maps.go +++ /dev/null @@ -1,120 +0,0 @@ -package rendermaps - -import ( - "math" - "sync" - - "github.com/paulmach/orb" -) - -const ( - TileSourceURL = "http://mapscii.me/" - TileRange = 14 - POIMarker = "◉" - ProjectSize = 256.0 - MaxLat = 85.0511 - - MaxZoom = 17.0 - MinZoom = 1.0 -) - -type tileJob struct { - tile *Tile - pos orb.Point -} - -type Renderer struct { - Canvas *Canvas - labelBuffer *LabelBuffer - jobs []tileJob - - tileSize float64 - zoom float64 -} - -func RenderMap(width, height int, lat, lon float64, zoom float64) *Renderer { - canvas := NewCanvas(width*pixelWidthPerChar, height*pixelHeightPerChar) // Canvas is in pixels (2x4 per char) - labelBuffer := NewLabelBuffer() - - z := baseZoom(zoom) - centerX, centerY := ll2tile(lon, lat, z) - tileSize := tilesizeAtZoom(zoom) - gridSize := math.Pow(2, float64(z)) - - fetchedTiles := make(chan tileJob) - var wg sync.WaitGroup - - for ty := math.Floor(centerY) - 1; ty <= math.Floor(centerY)+1; ty++ { - for tx := math.Floor(centerX) - 1; tx <= math.Floor(centerX)+1; tx++ { - tileX := int(math.Mod(tx, gridSize)) - if tileX < 0 { - tileX += int(gridSize) - } - tileY := int(ty) - - if tileY < 0 || tileY >= int(gridSize) { - continue - } - - wg.Add(1) - go func(z, x, y int, tx, ty float64) { - defer wg.Done() - tile, err := gTs.GetTile(z, x, y) - if err == nil { - pos := orb.Point{ - float64(canvas.width)/2 - (centerX-tx)*tileSize, - float64(canvas.height)/2 - (centerY-ty)*tileSize, - } - fetchedTiles <- tileJob{tile: tile, pos: pos} - } - }(z, tileX, tileY, tx, ty) - } - } - - go func() { - wg.Wait() - close(fetchedTiles) - }() - - var jobs []tileJob - for job := range fetchedTiles { - jobs = append(jobs, job) - } - - return &Renderer{ - Canvas: canvas, - labelBuffer: labelBuffer, - jobs: jobs, - tileSize: tileSize, - zoom: zoom, - } -} - -func RenderMapOneshot(width, height int, lat, lon float64, zoom float64) string { - renderer := RenderMap(width, height, lat, lon, zoom) - drawOrder := []string{"landuse", "water", "building", "road", "admin", "place_label", "poi_label"} - renderer.Draw(drawOrder) - return renderer.Frame() -} - -// drawOrder := []string{"landuse", "water", "building", "road", "admin", "place_label", "poi_label"} - -func (r *Renderer) Draw(drawOrder []string) { - for _, layerName := range drawOrder { - for _, job := range r.jobs { - renderTileLayer(r.Canvas, r.labelBuffer, job.tile, job.pos, r.tileSize, r.zoom, layerName) - } - } -} - -func (r *Renderer) Frame() string { - return r.Canvas.Frame() -} - -/* func RenderMapString(width, height int, lat, lon float64, zoom float64) (string, error) { - canvas, err := RenderMap(width, height, lat, lon, zoom) - if err != nil { - return "", err - } - return canvas.Frame(), nil -} */ diff --git a/rendermaps/proj.go b/rendermaps/proj.go deleted file mode 100644 index febd54d..0000000 --- a/rendermaps/proj.go +++ /dev/null @@ -1,190 +0,0 @@ -package rendermaps - -import ( - "math" - - "github.com/paulmach/orb" -) - -const ( - pixelWidthPerChar = 2 - pixelHeightPerChar = 4 -) - -// yMercatorNormalized calculates the normalized y-coordinate in Mercator projection (from 0 to 1). -// This is derived from the formula in mapscii/src/utils.js ll2tile(). -func yMercatorNormalized(lat float64) float64 { - latRad := lat * math.Pi / 180 - return (1 - math.Log(math.Tan(latRad)+1/math.Cos(latRad))/math.Pi) / 2 -} - -// FocusOn calculates the optimal center latitude, longitude, and zoom level to fit -// two coordinates within a given view size. The view size is provided in terminal -// characters (width, height). -func FocusOn(lat1, lon1, lat2, lon2 float64, viewWidthChars, viewHeightChars int) (centerLat, centerLon, zoom float64) { - // Apply some padding so the points are not at the very edge of the map. - // A value of 0.8 means the bounding box will take up 80% of the view. - padding := 0.8 - viewWidthPixels := float64(viewWidthChars*pixelWidthPerChar) * padding - viewHeightPixels := float64(viewHeightChars*pixelHeightPerChar) * padding - - // If the two points are (almost) the same, we can't calculate a span. - // Default to a fixed high zoom level centered on the point. - if math.Abs(lat1-lat2) < 1e-6 && math.Abs(lon1-lon2) < 1e-6 { - return lat1, lon1, MaxZoom // A good default zoom for a single point - } - - // 1. Calculate the bounding box, spans, and center point. - minLat := math.Min(lat1, lat2) - maxLat := math.Max(lat1, lat2) - centerLat = (minLat + maxLat) / 2 - - // The latitude span in normalized Mercator coordinates. - latSpanNorm := math.Abs(yMercatorNormalized(maxLat) - yMercatorNormalized(minLat)) - - // Handle longitude carefully due to the antimeridian (180° longitude). - var lonSpan float64 - if math.Abs(lon1-lon2) > 180 { - // The shortest path crosses the antimeridian. - maxLonVal := math.Max(lon1, lon2) - minLonVal := math.Min(lon1, lon2) - lonSpan = 360 - (maxLonVal - minLonVal) - centerLon = (maxLonVal + minLonVal + 360) / 2 - if centerLon > 180 { - centerLon -= 360 - } - } else { - // The path does not cross the antimeridian. - lonSpan = math.Abs(lon1 - lon2) - centerLon = (lon1 + lon2) / 2 - } - - // 2. Calculate the required zoom level. - // This is done by finding the necessary "world size" in pixels for both - // width and height, taking the smaller of the two, and then converting that - // back to a zoom level. - var worldSizeForLon, worldSizeForLat float64 - - if lonSpan > 0 { - worldSizeForLon = viewWidthPixels * 360 / lonSpan - } - if latSpanNorm > 0 { - worldSizeForLat = viewHeightPixels / latSpanNorm - } - - // Determine the constraining world size. - var worldSize float64 - if worldSizeForLon > 0 && worldSizeForLat > 0 { - worldSize = math.Min(worldSizeForLon, worldSizeForLat) - } else if worldSizeForLon > 0 { - worldSize = worldSizeForLon - } else { - worldSize = worldSizeForLat - } - - // If worldSize is still zero, we can't calculate a zoom. - if worldSize <= 0 { - return centerLat, centerLon, MaxZoom - } - - // Convert world size to zoom level using the formula: worldSize = tileSize * 2^zoom - zoom = math.Log2(worldSize / ProjectSize) - - // Clamp zoom to the valid range. - if zoom > MaxZoom { - zoom = MaxZoom - } - if zoom < MinZoom { - zoom = MinZoom - } - - return centerLat, centerLon, zoom -} - -// tileCoord represents a Web Mercator tile coordinate at a specific zoom level. -type tileCoord struct { - X, Y, Z float64 -} - -/* func clampLat(lat float64) float64 { - // The Mercator projection is undefined at the poles. - // This range is the standard for web maps (e.g., Google, OpenStreetMap). - const maxMercatorLat = 85.05112878 - if lat > maxMercatorLat { - return maxMercatorLat - } - if lat < -maxMercatorLat { - return -maxMercatorLat - } - return lat -} */ - -// llToTile converts a latitude/longitude pair to a tile coordinate at a given integer zoom level. -// This is a port of the formula from mapscii/src/utils.js. -/* func llToTile(lon, lat float64, zoom int) tileCoord { - clampedLat := clampLat(lat) - - zoomFloat := float64(zoom) - x := (lon + 180) / 360 * math.Pow(2, zoomFloat) - - latRad := clampedLat * math.Pi / 180 // Use the clamped latitude - y := (1 - math.Log(math.Tan(latRad)+1/math.Cos(latRad))/math.Pi) / 2 * math.Pow(2, zoomFloat) - - return tileCoord{X: x, Y: y, Z: zoomFloat} -} - -func tileSizeAtZoom(zoom float64, baseZoom int) float64 { - return 256.0 * math.Pow(2, zoom-float64(baseZoom)) -} */ - -// GeoToPixel converts a single lat/lon coordinate to an absolute (x, y) pixel coordinate on the canvas. -// It takes into account the map's center, zoom, and the canvas dimensions. -func geoToPixel(lat, lon float64, centerLat, centerLon, zoom float64, canvasWidth, canvasHeight int) orb.Point { - // --- Step 1: Define projection constants and helpers --- - const TILE_SIZE = 256.0 - clampLat := func(lat float64) float64 { - const maxMercatorLat = 85.05112878 - if lat > maxMercatorLat { - return maxMercatorLat - } - if lat < -maxMercatorLat { - return -maxMercatorLat - } - return lat - } - - // --- Step 2: Calculate intermediate projection values --- - // Get the integer zoom level for base tile calculations. - baseZoomVal := int(math.Floor(zoom)) - - // Calculate the effective size of a tile in pixels for the given fractional zoom. - effectiveTileSize := TILE_SIZE * math.Pow(2, zoom-float64(baseZoomVal)) - - // --- Step 3: Project Center and Target Point to Mercator Tile Coordinates --- - // Project the map's center point. - //centerLonRad := centerLon * math.Pi / 180 - centerLatRad := clampLat(centerLat) * math.Pi / 180 - centerTileX := (centerLon + 180) / 360 * math.Pow(2, float64(baseZoomVal)) - centerTileY := (1 - math.Log(math.Tan(centerLatRad)+1/math.Cos(centerLatRad))/math.Pi) / 2 * math.Pow(2, float64(baseZoomVal)) - - // Project the target point (the one we want to draw). - //lonRad := lon * math.Pi / 180 - latRad := clampLat(lat) * math.Pi / 180 - pointTileX := (lon + 180) / 360 * math.Pow(2, float64(baseZoomVal)) - pointTileY := (1 - math.Log(math.Tan(latRad)+1/math.Cos(latRad))/math.Pi) / 2 * math.Pow(2, float64(baseZoomVal)) - - // --- Step 4: Calculate the pixel offset from the center of the screen --- - // Find the distance between the center and the target point in tile units. - dxInTiles := pointTileX - centerTileX - dyInTiles := pointTileY - centerTileY - - // Convert this tile distance into a pixel distance. - dxInPixels := dxInTiles * effectiveTileSize - dyInPixels := dyInTiles * effectiveTileSize - - // The final coordinate is the center of the canvas plus the pixel distance. - finalX := float64(canvasWidth)/2 + dxInPixels - finalY := float64(canvasHeight)/2 + dyInPixels - - return orb.Point{finalX, finalY} -} \ No newline at end of file diff --git a/rendermaps/style.go b/rendermaps/style.go deleted file mode 100644 index 58d5688..0000000 --- a/rendermaps/style.go +++ /dev/null @@ -1,327 +0,0 @@ -package rendermaps - -import ( - "encoding/json" - "fmt" - "reflect" - - _ "embed" - - "github.com/paulmach/orb/geojson" -) - -type StyleLayer struct { - ID string `json:"id"` - Ref string `json:"ref"` - Type string `json:"type"` - SourceLayer string `json:"source-layer"` - MinZoom float64 `json:"minzoom"` - MaxZoom float64 `json:"maxzoom"` - Filter []any `json:"filter"` - Paint map[string]any `json:"paint"` - Layout map[string]any `json:"layout"` - - AppliesTo FilterFunc -} - -type styleJSON struct { - Name string `json:"name"` - Constants map[string]string `json:"constants"` - Layers []json.RawMessage `json:"layers"` -} - -type Styler struct { - styleByID map[string]*StyleLayer - styleByLayer map[string][]*StyleLayer - StyleName string -} - -func NewStyler(styleData []byte) (*Styler, error) { - var s styleJSON - if err := json.Unmarshal(styleData, &s); err != nil { - return nil, fmt.Errorf("failed to unmarshal style: %w", err) - } - - styler := &Styler{ - styleByID: make(map[string]*StyleLayer), - styleByLayer: make(map[string][]*StyleLayer), - StyleName: s.Name, - } - - if s.Constants == nil { - s.Constants = make(map[string]string) - } - - for _, rawLayer := range s.Layers { - // 1. Replace constants in the raw JSON of the layer. - processedLayerBytes, err := replaceConstantsInJSON(s.Constants, rawLayer) - if err != nil { - return nil, fmt.Errorf("failed to replace constants for layer: %w", err) - } - - var layer StyleLayer - if err := json.Unmarshal(processedLayerBytes, &layer); err != nil { - return nil, fmt.Errorf("failed to unmarshal layer from %s: %w", string(processedLayerBytes), err) - } - - // 2. Handle layer referencing (`ref` property). - if layer.Ref != "" { - if refLayer, ok := styler.styleByID[layer.Ref]; ok { - // Inherit properties from the referenced layer if they are not set. - if layer.Type == "" { - layer.Type = refLayer.Type - } - if layer.SourceLayer == "" { - layer.SourceLayer = refLayer.SourceLayer - } - if layer.MinZoom == 0 && refLayer.MinZoom != 0 { - layer.MinZoom = refLayer.MinZoom - } - if layer.MaxZoom == 0 && refLayer.MaxZoom != 0 { - layer.MaxZoom = refLayer.MaxZoom - } - if layer.Filter == nil { - layer.Filter = refLayer.Filter - } - } - } - - // 3. Compile the filter expression into a function. - layer.AppliesTo, err = CompileFilter(layer.Filter) - if err != nil { - layer.AppliesTo = func(f *geojson.Feature) bool { return true } - } - - // 4. Index the processed layer for fast lookup. - if layer.SourceLayer != "" { - styler.styleByLayer[layer.SourceLayer] = append(styler.styleByLayer[layer.SourceLayer], &layer) - } - styler.styleByID[layer.ID] = &layer - } - - return styler, nil -} - -// can return nil -func (s *Styler) GetStyleFor(sourceLayerName string, feature *geojson.Feature) *StyleLayer { - layers, ok := s.styleByLayer[sourceLayerName] - if !ok { - return nil - } - - for _, layer := range layers { - if layer.AppliesTo(feature) { - return layer - } - } - - return nil -} - -func (l *StyleLayer) GetPaintProperty(key string, defaultValue string) string { - if l.Paint == nil { - return defaultValue - } - if value, ok := l.Paint[key]; ok { - if strValue, isString := value.(string); isString { - return strValue - } - } - return defaultValue -} - -func replaceConstantsInJSON(constants map[string]string, raw json.RawMessage) (json.RawMessage, error) { - var node any - if err := json.Unmarshal(raw, &node); err != nil { - return nil, err - } - doReplace(constants, node) - return json.Marshal(node) -} - -func doReplace(constants map[string]string, node any) { - switch n := node.(type) { - case map[string]any: - for key, val := range n { - if strVal, ok := val.(string); ok && len(strVal) > 0 && strVal[0] == '@' { - if constVal, found := constants[strVal]; found { - n[key] = constVal - } - } else { - doReplace(constants, val) // Recurse - } - } - case []any: - for _, item := range n { - doReplace(constants, item) // Recurse - } - } -} - -type FilterFunc func(feature *geojson.Feature) bool - -func CompileFilter(filter []any) (FilterFunc, error) { - if len(filter) == 0 { - return func(f *geojson.Feature) bool { return true }, nil - } - - op, ok := filter[0].(string) - if !ok { - return nil, fmt.Errorf("filter operator must be a string, got %T", filter[0]) - } - - switch op { - case "all": - subFilters, err := compileSubFilters(filter) - if err != nil { - return nil, err - } - return func(f *geojson.Feature) bool { - for _, sf := range subFilters { - if !sf(f) { - return false - } - } - return true - }, nil - - case "any": - subFilters, err := compileSubFilters(filter) - if err != nil { - return nil, err - } - return func(f *geojson.Feature) bool { - for _, sf := range subFilters { - if sf(f) { - return true - } - } - return false - }, nil - - case "none": - subFilters, err := compileSubFilters(filter) - if err != nil { - return nil, err - } - return func(f *geojson.Feature) bool { - for _, sf := range subFilters { - if sf(f) { - return false - } - } - return true - }, nil - - case "==", "!=": - if len(filter) != 3 { - return nil, fmt.Errorf("'%s' filter expects 2 arguments, got %d", op, len(filter)-1) - } - key, ok := filter[1].(string) - if !ok { - return nil, fmt.Errorf("'%s' filter key must be a string, got %T", op, filter[1]) - } - val := filter[2] - return func(f *geojson.Feature) bool { - prop, exists := f.Properties[key] - isEqual := exists && reflect.DeepEqual(prop, val) - if op == "==" { - return isEqual - } - return !isEqual - }, nil - - case ">", ">=", "<", "<=": - if len(filter) != 3 { - return nil, fmt.Errorf("'%s' filter expects 2 arguments", op) - } - key, ok := filter[1].(string) - if !ok { - return nil, fmt.Errorf("'%s' filter key must be a string", op) - } - filterVal, ok := filter[2].(float64) // JSON numbers are parsed as float64 - if !ok { - return nil, fmt.Errorf("'%s' filter value must be a number, got %T", op, filter[2]) - } - return func(f *geojson.Feature) bool { - prop, exists := f.Properties[key] - if !exists { - return false - } - propVal, ok := prop.(float64) - if !ok { // Property exists but is not a number - return false - } - switch op { - case ">": - return propVal > filterVal - case ">=": - return propVal >= filterVal - case "<": - return propVal < filterVal - case "<=": - return propVal <= filterVal - } - return false - }, nil - - case "in", "!in": - if len(filter) < 3 { - return nil, fmt.Errorf("'%s' filter expects at least 2 arguments", op) - } - key, ok := filter[1].(string) - if !ok { - return nil, fmt.Errorf("'%s' filter key must be a string", op) - } - values := make(map[any]struct{}) - for _, v := range filter[2:] { - values[v] = struct{}{} - } - return func(f *geojson.Feature) bool { - prop, exists := f.Properties[key] - if !exists { - return op == "!in" // 'in' is false, '!in' is true if key is missing - } - _, found := values[prop] - if op == "in" { - return found - } - return !found - }, nil - - case "has", "!has": - if len(filter) != 2 { - return nil, fmt.Errorf("'%s' filter expects 1 argument", op) - } - key, ok := filter[1].(string) - if !ok { - return nil, fmt.Errorf("'%s' filter key must be a string", op) - } - return func(f *geojson.Feature) bool { - _, exists := f.Properties[key] - if op == "has" { - return exists - } - return !exists - }, nil - - default: - return nil, fmt.Errorf("unsupported filter operator: %s", op) - } -} - -func compileSubFilters(filter []any) ([]FilterFunc, error) { - subFilters := make([]FilterFunc, 0, len(filter)-1) - for i, subFilterExpr := range filter[1:] { - expr, ok := subFilterExpr.([]any) - if !ok { - return nil, fmt.Errorf("sub-filter at index %d is not a valid expression", i) - } - subFilter, err := CompileFilter(expr) - if err != nil { - return nil, err - } - subFilters = append(subFilters, subFilter) - } - return subFilters, nil -} \ No newline at end of file diff --git a/rendermaps/style.json b/rendermaps/style.json deleted file mode 100644 index 4842bfd..0000000 --- a/rendermaps/style.json +++ /dev/null @@ -1,1560 +0,0 @@ -{ - "name": "dark", - "constants": { - "@admin_level_2": "#fff", - "@admin_level_2_disputed": "#fff", - "@admin_level_2_maritime": "#9bf", - "@admin_level_3": "#aac", - "@admin_level_3_maritime": "#8af", - "@admin_level_4": "#777", - - "@aeroway_fill": "#f0ede9", - "@aeroway_runway": "#f0ede9", - "@aeroway_taxiway": "#f0ede9", - - "@airport_label": "#666", - - "@background": "#000", - - "@bridge_link": "#fea", - "@bridge_major_rail": "#bbb", - "@bridge_major_rail_hatching": "#bbb", - "@bridge_motorway": "#fc8", - "@bridge_motorway_link": "#fc8", - "@bridge_path_pedestrian": "#cba", - "@bridge_secondary_tertiary": "#fea", - "@bridge_service_track": "#fff", - "@bridge_street": "#fff", - "@bridge_trunk_primary": "#fea", - - "@building": "#99b", - - "@country_label_1": "#ff0", - "@country_label_2": "#ff0", - "@country_label_3": "#ff0", - "@country_label_4": "#ff0", - - "@landuse_cemetery": "#e0e4dd", - "@landuse_hospital": "#d9b", - "@landuse_overlay_national_park": "#d8e8c8", - "@landuse_park": "#7b5", - "@landuse_school": "#f0e8f8", - "@landuse_wood": "#6a4", - - "@marine_label_4": "#74aee9", - "@marine_label_line_1": "#74aee9", - "@marine_label_line_2": "#74aee9", - "@marine_label_line_3": "#74aee9", - "@marine_label_line_4": "#74aee9", - "@marine_label_point_1": "#74aee9", - "@marine_label_point_2": "#74aee9", - "@marine_label_point_3": "#74aee9", - - "@place_label_city": "#f00", - "@place_label_town": "#d33", - "@place_label_village": "#c33", - "@place_label_other": "#b33", - - "@poi_label_1": "#ff0", - "@poi_label_2": "#ee0", - "@poi_label_3": "#cc0", - "@poi_label_4": "#aa0", - - "@rail_station_label": "#666", - - "@road_label": "#765", - "@road_link": "#fea", - "@road_major_rail": "#bbb", - "@road_major_rail_hatching": "#bbb", - "@road_motorway": "#fc8", - "@road_motorway_link": "#fc8", - "@road_path_pedestrian": "#cba", - "@road_secondary_tertiary": "#fea", - "@road_service_track": "#fff", - "@road_street": "#fff", - "@road_trunk_primary": "#fea", - - "@tunnel_link": "#fff4c6", - "@tunnel_major_rail": "#bbb", - "@tunnel_major_rail_hatching": "#bbb", - "@tunnel_motorway": "#ffdaa6", - "@tunnel_motorway_link": "#fc8", - "@tunnel_path_pedestrian": "#cba", - "@tunnel_secondary_tertiary": "#fff4c6", - "@tunnel_service_track": "#fff", - "@tunnel_street": "#fff", - "@tunnel_trunk_primary": "#fff4c6", - - "@water": "#5f87ff", - "@water_label": "#74aee9", - - "@waterway": "#a0c8f0", - "@waterway_river": "#a0c8f0", - "@waterway_stream_canal": "#a0c8f0" - }, - "layers": [ - { - "type": "background", - "id": "background", - "paint": { - "background-color": "@background" - } - }, - { - "type": "fill", - "id": "landuse_overlay_national_park", - "paint": { - "fill-color": "@landuse_overlay_national_park" - }, - "source-layer": "landuse_overlay", - "filter": [ - "==", - "class", - "national_park" - ] - }, - { - "type": "fill", - "id": "landuse_park", - "paint": { - "fill-color": "@landuse_park" - }, - "source-layer": "landuse", - "filter": [ - "==", - "class", - "park" - ] - }, - { - "type": "line", - "id": "landuse_cemetery", - "paint": { - "fill-color": "@landuse_cemetery" - }, - "source-layer": "landuse", - "filter": [ - "==", - "class", - "cemetery" - ] - }, - { - "type": "line", - "id": "landuse_hospital", - "paint": { - "line-color": "@landuse_hospital" - }, - "source-layer": "landuse", - "filter": [ - "==", - "class", - "hospital" - ] - }, - { - "type": "line", - "id": "landuse_school", - "paint": { - "line-color": "@landuse_school" - }, - "source-layer": "landuse", - "filter": [ - "==", - "class", - "school" - ] - }, - { - "type": "line", - "id": "landuse_wood", - "paint": { - "line-color": "@landuse_wood" - }, - "source-layer": "landuse", - "filter": [ - "==", - "class", - "wood" - ] - }, - { - "type": "line", - "id": "waterway", - "paint": { - "line-color": "@waterway" - }, - "source-layer": "waterway", - "filter": [ - "all", - [ - "!=", - "class", - "river" - ], - [ - "!=", - "class", - "stream" - ], - [ - "!=", - "class", - "canal" - ] - ] - }, - { - "type": "line", - "id": "waterway_river", - "paint": { - "line-color": "@waterway_river" - }, - "source-layer": "waterway", - "filter": [ - "==", - "class", - "river" - ] - }, - { - "type": "line", - "id": "waterway_stream_canal", - "paint": { - "line-color": "@waterway_stream_canal" - }, - "source-layer": "waterway", - "filter": [ - "in", - "class", - "stream", - "canal" - ] - }, - { - "type": "fill", - "id": "water", - "paint": { - "fill-color": "@water" - }, - "source-layer": "water" - }, - { - "type": "fill", - "id": "aeroway_fill", - "paint": { - "fill-color": "@aeroway_fill" - }, - "source-layer": "aeroway", - "filter": [ - "==", - "$type", - "Polygon" - ], - "minzoom": 11 - }, - { - "type": "line", - "id": "aeroway_runway", - "paint": { - "line-color": "@aeroway_runway" - }, - "source-layer": "aeroway", - "filter": [ - "all", - [ - "==", - "$type", - "LineString" - ], - [ - "==", - "type", - "runway" - ] - ], - "minzoom": 11 - }, - { - "type": "line", - "id": "aeroway_taxiway", - "paint": { - "line-color": "@aeroway_taxiway" - }, - "source-layer": "aeroway", - "filter": [ - "all", - [ - "==", - "$type", - "LineString" - ], - [ - "==", - "type", - "taxiway" - ] - ], - "minzoom": 11 - }, - { - "type": "line", - "id": "building", - "paint": { - "line-color": "@building" - }, - "source-layer": "building" - }, - { - "type": "line", - "id": "tunnel_path_pedestrian", - "paint": { - "line-color": "@tunnel_path_pedestrian" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "$type", - "LineString" - ], - [ - "all", - [ - "==", - "structure", - "tunnel" - ], - [ - "in", - "class", - "path", - "pedestrian" - ] - ] - ] - }, - { - "type": "line", - "id": "tunnel_motorway_link", - "paint": { - "line-color": "@tunnel_motorway_link" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "structure", - "tunnel" - ], - [ - "==", - "class", - "motorway_link" - ] - ] - }, - { - "type": "line", - "id": "tunnel_service_track", - "paint": { - "line-color": "@tunnel_service_track" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "structure", - "tunnel" - ], - [ - "in", - "class", - "service", - "track" - ] - ] - }, - { - "type": "line", - "id": "tunnel_link", - "paint": { - "line-color": "@tunnel_link" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "structure", - "tunnel" - ], - [ - "==", - "class", - "link" - ] - ] - }, - { - "type": "line", - "id": "tunnel_street", - "paint": { - "line-color": "@tunnel_street" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "structure", - "tunnel" - ], - [ - "in", - "class", - "street", - "street_limited" - ] - ] - }, - { - "type": "line", - "id": "tunnel_secondary_tertiary", - "paint": { - "line-color": "@tunnel_secondary_tertiary" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "structure", - "tunnel" - ], - [ - "in", - "class", - "secondary", - "tertiary" - ] - ] - }, - { - "type": "line", - "id": "tunnel_trunk_primary", - "paint": { - "line-color": "@tunnel_trunk_primary" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "structure", - "tunnel" - ], - [ - "in", - "class", - "trunk", - "primary" - ] - ] - }, - { - "type": "line", - "id": "tunnel_motorway", - "paint": { - "line-color": "@tunnel_motorway" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "structure", - "tunnel" - ], - [ - "==", - "class", - "motorway" - ] - ] - }, - { - "type": "line", - "id": "tunnel_major_rail", - "paint": { - "line-color": "@tunnel_major_rail" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "structure", - "tunnel" - ], - [ - "in", - "class", - "major_rail", - "minor_rail" - ] - ] - }, - { - "type": "line", - "id": "tunnel_major_rail_hatching", - "paint": { - "line-color": "@tunnel_major_rail_hatching" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "structure", - "tunnel" - ], - [ - "in", - "class", - "major_rail", - "minor_rail" - ] - ] - }, - { - "type": "line", - "id": "road_path_pedestrian", - "paint": { - "line-color": "@road_path_pedestrian" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "$type", - "LineString" - ], - [ - "all", - [ - "in", - "class", - "path", - "pedestrian" - ], - [ - "!in", - "structure", - "bridge", - "tunnel" - ] - ] - ] - }, - { - "type": "line", - "id": "road_motorway_link", - "paint": { - "line-color": "@road_motorway_link" - }, - "source-layer": "road", - "minzoom": 12, - "filter": [ - "all", - [ - "==", - "class", - "motorway_link" - ], - [ - "!in", - "structure", - "bridge", - "tunnel" - ] - ] - }, - { - "type": "line", - "id": "road_service_track", - "paint": { - "line-color": "@road_service_track" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "in", - "class", - "service", - "track" - ], - [ - "!in", - "structure", - "bridge", - "tunnel" - ] - ] - }, - { - "type": "line", - "id": "road_link", - "paint": { - "line-color": "@road_link" - }, - "source-layer": "road", - "minzoom": 13, - "filter": [ - "all", - [ - "==", - "class", - "link" - ], - [ - "!in", - "structure", - "bridge", - "tunnel" - ] - ] - }, - { - "type": "line", - "id": "road_street", - "paint": { - "line-color": "@road_street" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "$type", - "LineString" - ], - [ - "all", - [ - "in", - "class", - "street", - "street_limited" - ], - [ - "!in", - "structure", - "bridge", - "tunnel" - ] - ] - ] - }, - { - "type": "line", - "id": "road_secondary_tertiary", - "paint": { - "line-color": "@road_secondary_tertiary" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "in", - "class", - "secondary", - "tertiary" - ], - [ - "!in", - "structure", - "bridge", - "tunnel" - ] - ] - }, - { - "type": "line", - "id": "road_trunk_primary", - "paint": { - "line-color": "@road_trunk_primary" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "in", - "class", - "trunk", - "primary" - ], - [ - "!in", - "structure", - "bridge", - "tunnel" - ] - ] - }, - { - "type": "line", - "id": "road_motorway", - "paint": { - "line-color": "@road_motorway" - }, - "source-layer": "road", - "minzoom": 5, - "filter": [ - "all", - [ - "==", - "class", - "motorway" - ], - [ - "!in", - "structure", - "bridge", - "tunnel" - ] - ] - }, - { - "type": "line", - "id": "road_major_rail", - "paint": { - "line-color": "@road_major_rail" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "class", - "major_rail" - ], - [ - "!in", - "structure", - "bridge", - "tunnel" - ] - ] - }, - { - "type": "line", - "id": "road_major_rail_hatching", - "paint": { - "line-color": "@road_major_rail_hatching" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "class", - "major_rail" - ], - [ - "!in", - "structure", - "bridge", - "tunnel" - ] - ] - }, - { - "type": "line", - "id": "bridge_path_pedestrian", - "paint": { - "line-color": "@bridge_path_pedestrian" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "$type", - "LineString" - ], - [ - "all", - [ - "==", - "structure", - "bridge" - ], - [ - "in", - "class", - "path", - "pedestrian" - ] - ] - ] - }, - { - "type": "line", - "id": "bridge_motorway_link", - "paint": { - "line-color": "@bridge_motorway_link" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "structure", - "bridge" - ], - [ - "==", - "class", - "motorway_link" - ] - ] - }, - { - "type": "line", - "id": "bridge_service_track", - "paint": { - "line-color": "@bridge_service_track" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "structure", - "bridge" - ], - [ - "in", - "class", - "service", - "track" - ] - ] - }, - { - "type": "line", - "id": "bridge_link", - "paint": { - "line-color": "@bridge_link" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "structure", - "bridge" - ], - [ - "==", - "class", - "link" - ] - ] - }, - { - "type": "line", - "id": "bridge_street", - "paint": { - "line-color": "@bridge_street" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "structure", - "bridge" - ], - [ - "in", - "class", - "street", - "street_limited" - ] - ] - }, - { - "type": "line", - "id": "bridge_secondary_tertiary", - "paint": { - "line-color": "@bridge_secondary_tertiary" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "structure", - "bridge" - ], - [ - "in", - "class", - "secondary", - "tertiary" - ] - ] - }, - { - "type": "line", - "id": "bridge_trunk_primary", - "paint": { - "line-color": "@bridge_trunk_primary" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "structure", - "bridge" - ], - [ - "in", - "class", - "trunk", - "primary" - ] - ] - }, - { - "type": "line", - "id": "bridge_motorway", - "paint": { - "line-color": "@bridge_motorway" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "structure", - "bridge" - ], - [ - "==", - "class", - "motorway" - ] - ] - }, - { - "type": "line", - "id": "bridge_major_rail", - "paint": { - "line-color": "@bridge_major_rail" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "structure", - "bridge" - ], - [ - "==", - "class", - "major_rail" - ] - ] - }, - { - "type": "line", - "id": "bridge_major_rail_hatching", - "paint": { - "line-color": "@bridge_major_rail_hatching" - }, - "source-layer": "road", - "filter": [ - "all", - [ - "==", - "structure", - "bridge" - ], - [ - "==", - "class", - "major_rail" - ] - ] - }, - { - "type": "line", - "id": "admin_level_3", - "paint": { - "line-color": "@admin_level_3" - }, - "source-layer": "admin", - "filter": [ - "all", - [ - ">=", - "admin_level", - 4 - ], - [ - "==", - "maritime", - 0 - ] - ] - }, - { - "type": "line", - "id": "admin_level_3", - "paint": { - "line-color": "@admin_level_3" - }, - "source-layer": "admin", - "filter": [ - "all", - [ - "==", - "admin_level", - 3 - ], - [ - "==", - "maritime", - 0 - ] - ] - }, - { - "type": "line", - "id": "admin_level_2", - "paint": { - "line-color": "@admin_level_2" - }, - "source-layer": "admin", - "filter": [ - "all", - [ - "==", - "admin_level", - 2 - ], - [ - "==", - "disputed", - 0 - ], - [ - "==", - "maritime", - 0 - ] - ] - }, - { - "type": "line", - "id": "admin_level_2_disputed", - "paint": { - "line-color": "@admin_level_2_disputed" - }, - "source-layer": "admin", - "filter": [ - "all", - [ - "==", - "admin_level", - 2 - ], - [ - "==", - "disputed", - 1 - ], - [ - "==", - "maritime", - 0 - ] - ] - }, - { - "type": "line", - "id": "admin_level_3_maritime", - "paint": { - "line-color": "@admin_level_3_maritime" - }, - "source-layer": "admin", - "filter": [ - "all", - [ - ">=", - "admin_level", - 3 - ], - [ - "==", - "maritime", - 1 - ] - ] - }, - { - "type": "line", - "id": "admin_level_2_maritime", - "paint": { - "line-color": "@admin_level_2_maritime" - }, - "source-layer": "admin", - "filter": [ - "all", - [ - "==", - "admin_level", - 2 - ], - [ - "==", - "maritime", - 1 - ] - ] - }, - { - "type": "symbol", - "id": "water_label", - "paint": { - "text-color": "@water_label" - }, - "source-layer": "water_label", - "filter": [ - "==", - "$type", - "Point" - ] - }, - { - "type": "symbol", - "id": "poi_label_4", - "paint": { - "text-color": "@poi_label_4" - }, - "source-layer": "poi_label", - "filter": [ - "all", - [ - "==", - "$type", - "Point" - ], - [ - "==", - "scalerank", - 4 - ] - ], - "minzoom": 16 - }, - { - "type": "symbol", - "id": "poi_label_3", - "paint": { - "text-color": "@poi_label_3" - }, - "source-layer": "poi_label", - "filter": [ - "all", - [ - "==", - "$type", - "Point" - ], - [ - "==", - "scalerank", - 3 - ] - ], - "minzoom": 15 - }, - { - "type": "symbol", - "id": "poi_label_2", - "paint": { - "text-color": "@poi_label_2" - }, - "source-layer": "poi_label", - "filter": [ - "all", - [ - "==", - "$type", - "Point" - ], - [ - "==", - "scalerank", - 2 - ] - ], - "minzoom": 14 - }, - { - "type": "symbol", - "id": "rail_station_label", - "paint": { - "text-color": "@rail_station_label" - }, - "source-layer": "rail_station_label" - }, - { - "type": "symbol", - "id": "poi_label_1", - "paint": { - "text-color": "@poi_label_1" - }, - "source-layer": "poi_label", - "filter": [ - "all", - [ - "==", - "$type", - "Point" - ], - [ - "==", - "scalerank", - 1 - ] - ], - "minzoom": 13 - }, - { - "type": "symbol", - "id": "airport_label", - "paint": { - "text-color": "@airport_label" - }, - "source-layer": "airport_label", - "filter": [ - "all", - [ - "==", - "$type", - "Point" - ], - [ - "in", - "scalerank", - 1, - 2, - 3 - ] - ], - "minzoom": 11 - }, - { - "type": "symbol", - "id": "road_label", - "paint": { - "text-color": "@road_label" - }, - "source-layer": "road_label", - "filter": [ - "!=", - "class", - "ferry" - ], - "minzoom": 15.5 - }, - { - "type": "symbol", - "id": "place_label_other", - "paint": { - "text-color": "@place_label_other" - }, - "source-layer": "place_label", - "filter": [ - "in", - "type", - "hamlet", - "suburb", - "neighbourhood", - "island", - "islet" - ] - }, - { - "type": "symbol", - "id": "place_label_village", - "paint": { - "text-color": "@place_label_village" - }, - "source-layer": "place_label", - "filter": [ - "==", - "type", - "village" - ] - }, - { - "type": "symbol", - "id": "place_label_town", - "paint": { - "text-color": "@place_label_town" - }, - "source-layer": "place_label", - "filter": [ - "==", - "type", - "town" - ] - }, - { - "type": "symbol", - "id": "place_label_city", - "paint": { - "text-color": "@place_label_city" - }, - "source-layer": "place_label", - "filter": [ - "==", - "type", - "city" - ] - }, - { - "type": "symbol", - "id": "marine_label_line_4", - "paint": { - "text-color": "@marine_label_line_4" - }, - "source-layer": "marine_label", - "filter": [ - "all", - [ - "==", - "$type", - "LineString" - ], - [ - ">=", - "labelrank", - 4 - ] - ] - }, - { - "type": "symbol", - "id": "marine_label_4", - "paint": { - "text-color": "@marine_label_4" - }, - "source-layer": "marine_label", - "filter": [ - "all", - [ - "==", - "$type", - "Point" - ], - [ - ">=", - "labelrank", - 4 - ] - ] - }, - { - "type": "symbol", - "id": "marine_label_line_3", - "paint": { - "text-color": "@marine_label_line_3" - }, - "source-layer": "marine_label", - "filter": [ - "all", - [ - "==", - "$type", - "LineString" - ], - [ - "==", - "labelrank", - 3 - ] - ] - }, - { - "type": "symbol", - "id": "marine_label_point_3", - "paint": { - "text-color": "@marine_label_point_3" - }, - "source-layer": "marine_label", - "filter": [ - "all", - [ - "==", - "$type", - "Point" - ], - [ - "==", - "labelrank", - 3 - ] - ] - }, - { - "type": "symbol", - "id": "marine_label_line_2", - "paint": { - "text-color": "@marine_label_line_2" - }, - "source-layer": "marine_label", - "filter": [ - "all", - [ - "==", - "$type", - "LineString" - ], - [ - "==", - "labelrank", - 2 - ] - ] - }, - { - "type": "symbol", - "id": "marine_label_point_2", - "paint": { - "text-color": "@marine_label_point_2" - }, - "source-layer": "marine_label", - "filter": [ - "all", - [ - "==", - "$type", - "Point" - ], - [ - "==", - "labelrank", - 2 - ] - ] - }, - { - "type": "symbol", - "id": "marine_label_line_1", - "paint": { - "text-color": "@marine_label_line_1" - }, - "source-layer": "marine_label", - "filter": [ - "all", - [ - "==", - "$type", - "LineString" - ], - [ - "==", - "labelrank", - 1 - ] - ] - }, - { - "type": "symbol", - "id": "marine_label_point_1", - "paint": { - "text-color": "@marine_label_point_1" - }, - "source-layer": "marine_label", - "filter": [ - "all", - [ - "==", - "$type", - "Point" - ], - [ - "==", - "labelrank", - 1 - ] - ] - }, - { - "type": "symbol", - "id": "country_label_4", - "paint": { - "text-color": "@country_label_4" - }, - "source-layer": "country_label", - "filter": [ - ">=", - "scalerank", - 4 - ] - }, - { - "type": "symbol", - "id": "country_label_3", - "paint": { - "text-color": "@country_label_3" - }, - "source-layer": "country_label", - "filter": [ - "==", - "scalerank", - 3 - ] - }, - { - "type": "symbol", - "id": "country_label_2", - "paint": { - "text-color": "@country_label_2" - }, - "source-layer": "country_label", - "filter": [ - "==", - "scalerank", - 2 - ] - }, - { - "type": "symbol", - "id": "country_label_1", - "paint": { - "text-color": "@country_label_1" - }, - "source-layer": "country_label", - "filter": [ - "==", - "scalerank", - 1 - ] - } - ] -} diff --git a/rendermaps/tile.go b/rendermaps/tile.go deleted file mode 100644 index ec9c432..0000000 --- a/rendermaps/tile.go +++ /dev/null @@ -1,221 +0,0 @@ -package rendermaps - -import ( - "bytes" - "compress/gzip" - "fmt" - "io" - "log" - "math" - "net/http" - "strings" - "sync" - "time" - - "github.com/mattn/go-runewidth" - "github.com/paulmach/orb" - "github.com/paulmach/orb/encoding/mvt" - "github.com/tidwall/rtree" - - _ "embed" -) - -// parser and compiler ofr Mapbox Map Style files -// https://www.mapbox.com/mapbox-gl-style-spec/ - -type StyledFeature struct { - Geometry orb.Geometry - Style *StyleLayer - Color, Label string -} - -type Tile struct { - mvt.Layer - Rtree *rtree.RTree -} - -type TileSource struct { - url string - styler *Styler - client *http.Client - cache map[string]*Tile - colorCache map[string]string - mu sync.Mutex -} - -//go:embed style.json -var styleJson []byte - -var ( - gStyler = makeNewStyler() - gTs = NewTileSource(TileSourceURL, gStyler) -) - -func makeNewStyler() (*Styler) { - styler, err := NewStyler(styleJson) - if err != nil { - log.Panicf("Failed to load embedded style file': %v", err) - } - return styler -} - -// TODO migrate to cache directory - -func NewTileSource(url string, styler *Styler) *TileSource { - return &TileSource{ - url: url, client: &http.Client{Timeout: 10 * time.Second}, styler: styler, - cache: make(map[string]*Tile), colorCache: make(map[string]string), - } -} - -func (ts *TileSource) GetTile(z, x, y int) (*Tile, error) { - var err error - var tile *Tile - var ok bool - - key := fmt.Sprintf("%d-%d-%d", z, x, y) - - //bench := time.Now() - ts.mu.Lock() - if tile, ok = ts.cache[key]; ok { - ts.mu.Unlock() - // cached - } else { - ts.mu.Unlock() - var body []byte - - if body, err = cacheGetKey(key); err == nil { - // cached - } else { - url := fmt.Sprintf("%s%d/%d/%d.pbf", ts.url, z, x, y) - req, _ := http.NewRequest("GET", url, nil) - req.Header.Set("User-Agent", "MapSCII-Go-MVP/1.0") - resp, err := ts.client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - body, err = io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - cacheInsertKey(key, body) // cache the response - } - - ts.mu.Lock() - tile = &Tile{Rtree: &rtree.RTree{}} - if err := tile.Load(body, ts.styler, ts.colorCache); err != nil { - return &Tile{Rtree: &rtree.RTree{}}, nil - } - - ts.cache[key] = tile - ts.mu.Unlock() - } - - return tile, nil -} - -func (t *Tile) Load(buffer []byte, styler *Styler, colorCache map[string]string) error { - gz, err := gzip.NewReader(bytes.NewReader(buffer)) - var data []byte - if err == nil { - data, err = io.ReadAll(gz) - gz.Close() - } else { - data = buffer - } - if err != nil { - return err - } - layers, err := mvt.Unmarshal(data) - if err != nil { - return err - } - for _, layer := range layers { - for _, feature := range layer.Features { - if feature.Properties == nil { - feature.Properties = make(map[string]interface{}) - } - - switch feature.Geometry.(type) { - case orb.Point, orb.MultiPoint: - feature.Properties["$type"] = "Point" - case orb.LineString, orb.MultiLineString: - feature.Properties["$type"] = "LineString" - case orb.Polygon, orb.MultiPolygon: - feature.Properties["$type"] = "Polygon" - } - - style := styler.GetStyleFor(layer.Name, feature) - if style == nil { - continue - } - - colorStr := style.GetPaintProperty("line-color", style.GetPaintProperty("fill-color", "#ffffff")) - colorCode, ok := colorCache[colorStr] - if !ok { - colorCode = hexToANSI(colorStr) - colorCache[colorStr] = colorCode - } - label, _ := feature.Properties["name"].(string) - styledFeat := &StyledFeature{ - Geometry: feature.Geometry, Style: style, Color: colorCode, Label: label, - } - bounds := feature.Geometry.Bound() - t.Rtree.Insert([2]float64{bounds.Min.X(), bounds.Min.Y()}, [2]float64{bounds.Max.X(), bounds.Max.Y()}, styledFeat) - } - } - if len(layers) > 0 { - t.Layer = *layers[0] - } - return nil -} - -type LabelBuffer struct { - tree *rtree.RTree -} - -func NewLabelBuffer() *LabelBuffer { - return &LabelBuffer{tree: &rtree.RTree{}} -} - -func (lb *LabelBuffer) WriteIfPossible(text string, x, y int) bool { - width := runewidth.StringWidth(text) - bounds := [2][2]float64{{float64(x - 1), float64(y - 1)}, {float64(x + width + 1), float64(y + 1)}} - var collision bool - lb.tree.Search(bounds[0], bounds[1], func(_, _ [2]float64, _ interface{}) bool { - collision = true - return false - }) - if !collision { - lb.tree.Insert(bounds[0], bounds[1], text) - return true - } - return false -} - -func baseZoom(zoom float64) int { - return min(int(TileRange), int(math.Floor(zoom))) -} - -func tilesizeAtZoom(zoom float64) float64 { - return ProjectSize * math.Pow(2, zoom-float64(baseZoom(zoom))) -} - -func ll2tile(lon, lat float64, zoom int) (float64, float64) { - latRad := lat * math.Pi / 180 - n := math.Pow(2, float64(zoom)) - xtile := (lon + 180) / 360 * n - ytile := (1 - math.Asinh(math.Tan(latRad))/math.Pi) / 2 * n - return xtile, ytile -} - -func hexToANSI(hex string) string { - hex = strings.TrimPrefix(hex, "#") - if len(hex) == 3 { - hex = string([]byte{hex[0], hex[0], hex[1], hex[1], hex[2], hex[2]}) - } - var r, g, b uint8 - fmt.Sscanf(hex, "%02x%02x%02x", &r, &g, &b) - return fmt.Sprintf("\x1b[38;2;%d;%d;%dm", r, g, b) -} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..292fe49 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..f1e67b1 --- /dev/null +++ b/shell.nix @@ -0,0 +1,33 @@ +{ + pkgs ? import { }, +}: + +let + overrides = (builtins.fromTOML (builtins.readFile ./rust-toolchain.toml)); +in +pkgs.callPackage ( + { + stdenv, + mkShell, + rustup, + rustPlatform, + }: + mkShell { + strictDeps = true; + nativeBuildInputs = [ + rustup + rustPlatform.bindgenHook + ]; + # libraries here + buildInputs = + [ + ]; + RUSTC_VERSION = overrides.toolchain.channel; + # https://github.com/rust-lang/rust-bindgen#environment-variables + shellHook = '' + export PATH="''${CARGO_HOME:-~/.cargo}/bin":"$PATH" + export PATH="''${RUSTUP_HOME:-~/.rustup}/toolchains/$RUSTC_VERSION-${stdenv.hostPlatform.rust.rustcTarget}/bin":"$PATH" + ''; + } +) { } + diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..6e0f80b --- /dev/null +++ b/src/app.rs @@ -0,0 +1,83 @@ +use std::io; +use ratatui::crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind}; +use ratatui::{ + buffer::Buffer, + layout::*, + style::Stylize, + symbols::border, + text::{Line, Text}, + widgets::{Block, Paragraph, Widget}, + DefaultTerminal, Frame, +}; + +#[derive(Debug, Default)] +pub struct App { + counter: u8, + exit: bool, +} + +impl App { + pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { + while !self.exit { + terminal.draw(|frame| self.draw(frame))?; + self.handle_events()?; + } + Ok(()) + } + + fn draw(&self, frame: &mut Frame) { + let layout = Layout::default() + .direction(Direction::Horizontal) + .constraints(vec![ + Constraint::Percentage(70), + Constraint::Percentage(30), + ]) + .split(frame.area()); + + frame.render_widget(self.build_main(), layout[0]); + frame.render_widget(self.build_sidebar(), layout[1]); + } + + fn build_sidebar(&self) -> impl Widget { + let text = Text::raw("this is a sidebar"); + + let block = Block::bordered() + .border_set(border::ROUNDED); + + Paragraph::new(text) + .block(block) + .centered() + } + + fn build_main(&self) -> impl Widget { + let text = Text::raw("this is the main window"); + + let block = Block::bordered() + .border_set(border::ROUNDED); + + Paragraph::new(text) + .block(block) + .centered() + } + + fn handle_events(&mut self) -> io::Result<()> { + match event::read()? { + Event::Key(key_event) if key_event.kind == KeyEventKind::Press => { + self.handle_key_event(key_event) + }, + _ => {} + }; + Ok(()) + } + + fn handle_key_event(&mut self, key_event: KeyEvent) { + match key_event.code { + KeyCode::Char('q') => self.exit(), + _ => {} + } + } + + fn exit(&mut self) { + self.exit = true; + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..338abb0 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,13 @@ +mod app; +mod maps; + +use std::io; +use app::App; + +fn main() -> io::Result<()> { + let mut terminal = ratatui::init(); + // create and run app + let app_result = App::default().run(&mut terminal); + ratatui::restore(); + app_result +} diff --git a/src/maps/cache.rs b/src/maps/cache.rs new file mode 100644 index 0000000..f557a15 --- /dev/null +++ b/src/maps/cache.rs @@ -0,0 +1,35 @@ +use std::env; +use std::fs; +use std::io; +use std::path::PathBuf; + +#[derive(Debug, Default)] +pub struct TileCache { + path: PathBuf +} + +impl TileCache { + pub fn create() -> io::Result { + /* build path */ + let mut cache_path = env::home_dir() + .ok_or(io::Error::new(io::ErrorKind::NotFound, "no home dir"))?; + cache_path.push(".cache/trip"); + fs::create_dir_all(&cache_path)?; + + Ok(Self{ + path: cache_path + }) + } + + pub fn insert_key(&self, key: String, val: &[u8]) -> io::Result<()> { + let path = self.path.join(key); + fs::write(path, val)?; + Ok(()) + } + + pub fn get_key(&self, key: String) -> io::Result> { + let path = self.path.join(key); + let val = fs::read(path)?; + Ok(val) + } +} diff --git a/src/maps/mod.rs b/src/maps/mod.rs new file mode 100644 index 0000000..b65f6e3 --- /dev/null +++ b/src/maps/mod.rs @@ -0,0 +1,3 @@ +pub mod cache; +pub mod proj; +pub mod style; diff --git a/src/maps/proj.rs b/src/maps/proj.rs new file mode 100644 index 0000000..0dfaa9a --- /dev/null +++ b/src/maps/proj.rs @@ -0,0 +1,147 @@ +use std::f64::consts::PI; + +const CHAR_PIXEL_WIDTH: i32 = 2; +const CHAR_PIXEL_HEIGHT: i32 = 4; +const MAX_ZOOM: f64 = 17.0; +const MIN_ZOOM: f64 = 1.0; +const PROJECT_SIZE: f64 = 256.0; +const TILE_SIZE: f64 = 256.0; +/* +* the Mercator projection is undefined at the poles. +* this range is the standard for web maps +*/ +const MAX_MERCATOR_LAT: f64 = 85.05112878; + +/* TileCoord represents a Web Mercator tile coordinate at a specific zoom level */ +pub struct TileCoord { + x: f64, + y: f64, + z: f64 +} + +pub struct Viewport { + lat: f64, + lon: f64, + zoom: f64 +} + +pub struct Point { + x: f64, + y: f64, +} + +/* +* calculates the normalised y-coordinate in Mercator projection (0 -> 1) +* this is derived from the formula in `mapscii/src/utils.js` `ll2tile()` +*/ +fn y_mercator_normalised(lat: f64) -> f64 { + let lat_rad = lat.to_radians(); + (1.0 - (lat_rad.tan() + 1.0 / lat_rad.cos()).ln() / PI) / 2.0 +} + +/* +* calculates the optimal centre lat, long and zoom level to fit 2x coords within a given +* view size. the view size is provided in terminal characters (width & height) +*/ +pub fn focus_on(lat1: f64, lon1: f64, lat2: f64, lon2: f64, view_width_chars: i32, view_height_chars: i32) -> Viewport { + /* pad so pts aren't at edge (0.8 => bounding box will take up 80% of viewport) */ + let padding = 0.8; + let view_width_pixels = (view_width_chars * CHAR_PIXEL_WIDTH) as f64 * padding; + let view_height_pixels = (view_height_chars * CHAR_PIXEL_HEIGHT) as f64 * padding; + + /* if pts are (almost) the same, default to fixed zoom centered on point */ + if (lat1 - lat2).abs() < 1e-6 && (lon1 - lon2).abs() < 1e-6 { + return Viewport{lat: lat1, lon: lon1, zoom: MAX_ZOOM} + } + + /* calc bbox, spans, centre point */ + let min_lat = f64::min(lat1, lat2); + let max_lat = f64::max(lat1, lat2); + let centre_lat = (min_lat + max_lat) / 2.0; + + /* the lat span in normalised Mercator coords */ + let lat_span_norm = (y_mercator_normalised(max_lat) - y_mercator_normalised(min_lat)).abs(); + + /* calc depends on antimeridian (180deg lon) */ + let lon_span: f64; + let mut centre_lon: f64; + if (lon1 - lon2).abs() > 180.0 { + /* shortest path crosses the antimeridian */ + let max_lon = f64::max(lon1, lon2); + let min_lon = f64::min(lon1, lon2); + lon_span = 360.0 - (max_lon - min_lon); + centre_lon = (max_lon + min_lon + 360.0) / 2.0; + + if centre_lon > 180.0 { + centre_lon -= 360.0; + } + } else { + /* path does not cross the antimeridian */ + lon_span = (lon1 - lon2).abs(); + centre_lon = (lon1 + lon2) / 2.0; + } + + /* find required zoom lvl */ + let mut lat_world_size = 0.0; + let mut lon_world_size = 0.0; + + if lon_span > 0.0 { + lon_world_size = view_width_pixels * 360.0 / lon_span; + } + if lat_span_norm > 0.0 { + lat_world_size = view_height_pixels / lat_span_norm; + } + + /* determine constraining world size */ + let world_size: f64; + if lon_world_size > 0.0 && lat_world_size > 0.0 { + world_size = f64::min(lon_world_size, lat_world_size); + } else if lon_world_size > 0.0 { + world_size = lon_world_size; + } else { + world_size = lat_world_size; + } + + /* if world size still 0, can't calc zoom -> use defaults */ + if world_size <= 0.0 { + return Viewport{lat: centre_lat, lon: centre_lon, zoom: MAX_ZOOM} + } + + let mut zoom = (world_size / PROJECT_SIZE).log2(); + zoom = zoom.clamp(MAX_ZOOM, MIN_ZOOM); + + Viewport{lat: lat1, lon: lon1, zoom} +} + +/* +* converts a single lat/lon coordinate to an absolute (x, y) pixel coordinate on the canvas. +* takes into account map centre, zoom and canvas dimensions +*/ +fn geo_to_pixel(lat: f64, lon: f64, viewport: &Viewport, canvas_width: i32, canvas_height: i32) -> Point { + /* zoom level for base tile calcs */ + let base_zoom = viewport.zoom.floor(); + /* effective size of a tile in px for given fractional zone */ + let effective_tile_size = TILE_SIZE * (viewport.zoom - base_zoom).powf(2.0); + + /* project the map's centre point */ + let centre_lat_rad = viewport.lat.clamp(-MAX_MERCATOR_LAT, MAX_MERCATOR_LAT).to_radians(); + let centre_tile_x = (viewport.lon + 180.0) / 360.0 * base_zoom.powf(2.0); + let centre_tile_y = (1.0 - (centre_lat_rad.tan() + 1.0 / centre_lat_rad.cos()).ln() / PI) / 2.0 * base_zoom.powf(2.0); + + /* project target point */ + let lat_rad = lat.clamp(-MAX_MERCATOR_LAT, MAX_MERCATOR_LAT).to_radians(); + let pt_tile_x = (lon + 180.0) / 360.0 * base_zoom.powf(2.0); + let pt_tile_y = (1.0 - (lat_rad.tan() + 1.0 / lat_rad.cos()).ln() / PI) / 2.0 * base_zoom.powf(2.0); + + /* calc pixel offset from screen centre */ + let dx_tiles = pt_tile_x - centre_tile_x; + let dy_tiles = pt_tile_y - centre_tile_y; + + let dx_px = dx_tiles * effective_tile_size; + let dy_px = dy_tiles * effective_tile_size; + + let final_x = canvas_width as f64 / 2.0 + dx_px; + let final_y = canvas_height as f64 / 2.0 + dy_px; + + Point{ x: final_x, y: final_y} +} diff --git a/src/maps/style.rs b/src/maps/style.rs new file mode 100644 index 0000000..2d0ca31 --- /dev/null +++ b/src/maps/style.rs @@ -0,0 +1,35 @@ +use geojson; +use std::collections::HashMap; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize)] +pub struct StyleLayer { + #[serde(rename = "id")] + layer_id: String, + #[serde(rename = "ref")] + layer_ref: String, + #[serde(rename = "type")] + layer_type: String, + #[serde(rename = "source-layer")] + source_layer: String, + #[serde(rename = "minzoom")] + min_zoom: f64, + #[serde(rename = "maxzoom")] + max_zoom: f64, + filter: Vec, + paint: HashMap, + layout: HashMap, +} + +#[derive(Deserialize, Serialize)] +struct StyleJSON { + name: String, + constants: HashMap, + layers: Vec, +} + +pub struct Styler { + style_by_id: HashMap>, + style_by_layer: HashMap>, + name: String, +} diff --git a/state/database_path.go b/state/database_path.go deleted file mode 100644 index ccca2e8..0000000 --- a/state/database_path.go +++ /dev/null @@ -1,19 +0,0 @@ -package state - -import ( - "path" - "runtime" -) - -func forceRelativeToRoot(s string) string { - _, filename, _, ok := runtime.Caller(0) - if !ok { - panic("unreachable") - } - return path.Join(path.Dir(filename), s) -} - -var ( - // this is exceptionally shitty code, but it allows our tests to actually get the database from root - DatabasePath = forceRelativeToRoot("../app.sqlite") -) diff --git a/styles/colours.go b/styles/colours.go deleted file mode 100644 index e10d52b..0000000 --- a/styles/colours.go +++ /dev/null @@ -1,201 +0,0 @@ -package styles - -import lg "github.com/charmbracelet/lipgloss" -import "strings" - -// TODO: some way to map colours to line name/id/mode something -// that corresponds to the output given in the `/trip` endpoint -// + add T6 colour - -// colours for each transport line -// source: https://opendata.transport.nsw.gov.au/developers/resources -const ( - // metro - MetroColour = "#168388" - - // sydney trains - T1Colour = "#F99D1C" - T2Colour = "#0098CD" - T3Colour = "#F37021" - T4Colour = "#005AA" - T5Colour = "#C4258F" - T6Colour = "#7F3D1B" - T7Colour = "#6F818E" - T8Colour = "#00954C" - T9Colour = "#D11F2F" - - BusColour = "#009ED7" - - // intercity trains - BlueMountainsColour = "#F99D1C" - CCNewcastleColour = "#D11F2F" - HunterColour = "#833134" - SouthCoastColour = "#005AA3" - SouthernHighlandsColour = "#00954C" - - // regional trains and coaches network - TrainsColour = "#F6891F" - CoachesColour = "#732A82" - - // ferries - F1Colour = "#00774B" - F2Colour = "#144734" - F3Colour = "#648C3C" - F4Colour = "#BFD730" - F5Colour = "#286142" - F6Colour = "#00AB51" - F7Colour = "#00B189" - F8Colour = "#55622B" - F9Colour = "#65B32E" - F10Colour = "#5AB031" // colour spec subject to change - StocktonColour = "#5AB031" - - // sydney light rail - L1Colour = "#BE1622" - L2Colour = "#DD1E25" - L3Colour = "#781140" - NLRColour = "#EE343F" - - WalkColour = "#4d4d4d" -) - -// flexbox colours -const ( - InactiveColour = lg.ANSIColor(8) - ActiveColour = lg.ANSIColor(7) -) - -var LineColours = map[string]string{ - // Metro - "Metro": MetroColour, - - "Bus": BusColour, - - // Sydney Trains - "T1": T1Colour, - "T2": T2Colour, - "T3": T3Colour, - "T4": T4Colour, - "T5": T5Colour, - "T6": T6Colour, - "T7": T7Colour, - "T8": T8Colour, - "T9": T9Colour, - - // Intercity - "BlueMountains": BlueMountainsColour, - "CCNewcastle": CCNewcastleColour, - "Hunter": HunterColour, - "SouthCoast": SouthCoastColour, - "SouthernHighlands": SouthernHighlandsColour, - - // Regional - "Trains": TrainsColour, - "Coaches": CoachesColour, - - // Ferries - "F1": F1Colour, - "F2": F2Colour, - "F3": F3Colour, - "F4": F4Colour, - "F5": F5Colour, - "F6": F6Colour, - "F7": F7Colour, - "F8": F8Colour, - "F9": F9Colour, - "F10": F10Colour, - "Stockton": StocktonColour, - - // Light Rail - "L1": L1Colour, - "L2": L2Colour, - "L3": L3Colour, - "NLR": NLRColour, - - "WALK": WalkColour, -} - -func LgColourForLine(line string) lg.Color { - line = strings.ToUpper(line) - var colour string - - switch { - // Metro lines like M1, M2, M3 - case strings.HasPrefix(line, "M"): - colour = MetroColour - - // Ferries (F1-F10) - case strings.HasPrefix(line, "F"): - if c, ok := LineColours[line]; ok { - colour = c - } - - // Light rail - case strings.HasPrefix(line, "L"): - if c, ok := LineColours[line]; ok { - colour = c - } - - // Trains (T1-T9) - case strings.HasPrefix(line, "T"): - if c, ok := LineColours[line]; ok { - colour = c - } - - // Explicit map fallback (covers intercity, regional, named lines) - default: - if c, ok := LineColours[line]; ok { - colour = c - } else if line == "WALK" { - colour = LineColours["WALK"] - } else { - colour = LineColours["Bus"] - } - } - - return lg.Color(colour) -} - - -func HexColourForLine(line string) string { - line = strings.ToUpper(line) - var colour string - - switch { - // Metro lines like M1, M2, M3 - case strings.HasPrefix(line, "M"): - colour = MetroColour - - // Ferries (F1-F10) - case strings.HasPrefix(line, "F"): - if c, ok := LineColours[line]; ok { - colour = c - } - - // Light rail - case strings.HasPrefix(line, "L"): - if c, ok := LineColours[line]; ok { - colour = c - } - - // Trains (T1-T9) - case strings.HasPrefix(line, "T"): - if c, ok := LineColours[line]; ok { - colour = c - } - - // Explicit map fallback (covers intercity, regional, named lines) - default: - if c, ok := LineColours[line]; ok { - colour = c - } else if line == "WALK" { - colour = LineColours["WALK"] - } else { - colour = LineColours["Bus"] - } - } - - return colour -} - - diff --git a/styles/flexbox.go b/styles/flexbox.go deleted file mode 100644 index c3712d3..0000000 --- a/styles/flexbox.go +++ /dev/null @@ -1,30 +0,0 @@ -package styles - -import lg "github.com/charmbracelet/lipgloss" - -// flexbox border styles -var ( - Border = lg.NewStyle(). - BorderStyle(lg.RoundedBorder()) -) - -// flexbox internal content styles -var ( - Prompt = lg.NewStyle(). - Bold(true). - Align(lg.Center) - WelcomeMain = lg.NewStyle(). - AlignHorizontal(lg.Center). - AlignVertical(lg.Center). - Align(lg.Center). - Inherit(Border) - WelcomeMainContent = lg.NewStyle(). - Align(lg.Center) - WelcomeSidebar = lg.NewStyle(). - PaddingRight(2). - PaddingLeft(2). - PaddingTop(1). - Inherit(Border) - WelcomeSidebarContent = lg.NewStyle() - LegBox = lg.NewStyle() -) diff --git a/styles/route.go b/styles/route.go deleted file mode 100644 index 02ff357..0000000 --- a/styles/route.go +++ /dev/null @@ -1,44 +0,0 @@ -package styles - -import ( - lg "github.com/charmbracelet/lipgloss" -) - -var ( - RouteTitle = lg.NewStyle(). - Align(lg.Left). - Bold(true) - RouteLegBox = lg.NewStyle(). - BorderStyle(lg.RoundedBorder()). - Align(lg.Left) -) - -func CreateLineHighlight(transit string) lg.Style { - colour := LgColourForLine(transit) - - style := lg.NewStyle().Background(colour).Bold(true) - return style -} - -func FormatRouteLeg(width int, transit string, bold bool) lg.Style { - return lg.NewStyle(). - BorderStyle(lg.RoundedBorder()). - Align(lg.Left). - Width(width). - PaddingRight(1). - PaddingLeft(1). - Bold(bold). - BorderForeground(LgColourForLine(transit)) // mid fix - -} - -func FormatRouteLegSelected(width int, transit string) lg.Style { - return lg.NewStyle(). - BorderStyle(lg.RoundedBorder()). - Align(lg.Left). - Width(width). - PaddingRight(1). - PaddingLeft(1). - BorderForeground(LgColourForLine(transit)) // mid fix - -} diff --git a/ui/destInput.go b/ui/destInput.go deleted file mode 100644 index 81e8f70..0000000 --- a/ui/destInput.go +++ /dev/null @@ -1,56 +0,0 @@ -package ui - -import ( - "github.com/76creates/stickers/flexbox" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" - "github.com/isobelmcrae/trip/styles" - "github.com/charmbracelet/log" -) - -type destInputState struct { - root *RootModel - input textinput.Model -} - -func (s *destInputState) Update(msg tea.Msg) (AppState, tea.Cmd){ - var cmd tea.Cmd - s.input, cmd = s.input.Update(msg) - - switch msg := msg.(type) { - case tea.KeyMsg: - if msg.Type == tea.KeyEnter { - log.Debug("User input", "input", s.input.Value()) - s.root.States.Push(newDestSelectState(s.root, s.input.Value())) - return s, cmd - } - } - - return s, cmd -} - -// Update the main window and the sidebar's content -func (s *destInputState) RenderCells(f *flexbox.FlexBox) { - sidebar := styles.WelcomeSidebarContent.Render(s.input.View()) - - f.GetRow(0).GetCell(1). - SetContent(styles.Prompt.Render("Where are you going?") + "\n\n" + sidebar). - SetStyle(styles.WelcomeSidebar) -} - -// creates a new welcome state which can then -// be pushed onto states -func newDestInputState(root *RootModel) AppState { - ti := textinput.New() - ti.Placeholder = "Enter destination stop..." - ti.Focus() - ti.Width = 30 - - return &destInputState{ - input: ti, - root: root, - } -} - - - diff --git a/ui/destSelect.go b/ui/destSelect.go deleted file mode 100644 index 9cfa6da..0000000 --- a/ui/destSelect.go +++ /dev/null @@ -1,119 +0,0 @@ -package ui - -import ( - "github.com/76creates/stickers/flexbox" - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/log" - // "github.com/charmbracelet/lipgloss" - "github.com/isobelmcrae/trip/styles" -) - - -type destSelectState struct { - root *RootModel - selectionList list.Model - listSize int - input string -} - -type destStopItem struct { - title string - id string -} - -func (sI destStopItem) Title() string { - return sI.title -} - -func (sI destStopItem) Description() string { - return sI.id -} - -func (sI destStopItem) FilterValue() string { - return sI.title -} - -func (s *destSelectState) Update(msg tea.Msg) (AppState, tea.Cmd){ - var cmd tea.Cmd - - s.selectionList, cmd = s.selectionList.Update(msg) - - switch msg := msg.(type) { - case tea.KeyMsg: - if msg.Type == tea.KeyEnter { - // create and push next state IF there are stops - if s.listSize == 0 { - s.root.States.Pop() - return s, cmd - } - - selectedItem := s.selectionList.SelectedItem() - - selectedID := selectedItem.(destStopItem).id - log.Debug("stop selected", "id", selectedID) - s.root.DestinationID = selectedID - - s.root.States.Push(newRouteState(s.root)) - - return s, cmd - } - - } - - return s, cmd -} - -// updates sidebar flexbox to display the selection list -func (s *destSelectState) RenderCells(f *flexbox.FlexBox) { - prompt := "Select stop:\n" - - // TODO: better way to store these values? - sidebarHeight := s.root.Sidebar.GetHeight() - sidebarWidth := s.root.Sidebar.GetWidth() - - s.selectionList.SetSize(sidebarWidth - 7, sidebarHeight - 10) - - sidebar := styles.WelcomeSidebarContent.Render(styles.Prompt.Render(prompt) + s.selectionList.View()) - - f.GetRow(0).GetCell(1). - SetContent(sidebar). - SetStyle(styles.WelcomeSidebar) -} - -// creates a new selection state which can then -// be pushed onto states -func newDestSelectState(root *RootModel, input string) AppState { - sl := list.New([]list.Item{}, list.NewDefaultDelegate(), 20, 10) - sl.SetShowTitle(false) - sl.SetShowHelp(false) - sl.SetShowStatusBar(false) - - m := &destSelectState{ - selectionList: sl, - root: root, - input: input, - } - - destSelectStop(m) - - return m -} - -// gets the stops which match the input string, formats them into a selection list -func destSelectStop(m *destSelectState) { - stops := m.root.Client.FindStop(m.input) - - m.listSize = len(stops) - if m.listSize == 0 { - log.Debug("No stops found") - } - - listItems := make([]list.Item, len(stops)) - for i, stop := range stops { - listItems[i] = destStopItem{ title: stop.Name, id: stop.ID } - } - - m.selectionList.SetItems(listItems) - m.selectionList.Select(0) -} diff --git a/ui/originInput.go b/ui/originInput.go deleted file mode 100644 index ea0c0b2..0000000 --- a/ui/originInput.go +++ /dev/null @@ -1,57 +0,0 @@ -package ui - -import ( - "github.com/76creates/stickers/flexbox" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" - "github.com/isobelmcrae/trip/styles" - "github.com/charmbracelet/log" -) - -type originInputState struct { - root *RootModel - input textinput.Model - output *string -} - -func (s *originInputState) Update(msg tea.Msg) (AppState, tea.Cmd){ - var cmd tea.Cmd - s.input, cmd = s.input.Update(msg) - - switch msg := msg.(type) { - case tea.KeyMsg: - if msg.Type == tea.KeyEnter { - log.Debug("User origin input", "input", s.input.Value()) - s.root.States.Push(newOriginSelectState(s.root, s.input.Value())) - return s, cmd - } - } - - return s, cmd -} - -// Update the main window and the sidebar's content -func (s *originInputState) RenderCells(f *flexbox.FlexBox) { - sidebar := styles.WelcomeSidebarContent.Render(s.input.View()) - - f.GetRow(0).GetCell(1). - SetContent(styles.Prompt.Render("Where are you?") + "\n\n" + sidebar). - SetStyle(styles.WelcomeSidebar) -} - -// creates a new welcome state which can then -// be pushed onto states -func newOriginInputState(root *RootModel) AppState { - ti := textinput.New() - ti.Placeholder = "Enter origin stop..." - ti.Focus() - ti.Width = 30 - - return &originInputState{ - input: ti, - root: root, - } -} - - - diff --git a/ui/originSelect.go b/ui/originSelect.go deleted file mode 100644 index 48ac5cf..0000000 --- a/ui/originSelect.go +++ /dev/null @@ -1,121 +0,0 @@ -package ui - -import ( - "github.com/76creates/stickers/flexbox" - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/log" - // "github.com/charmbracelet/lipgloss" - "github.com/isobelmcrae/trip/styles" -) - - -type originSelectState struct { - root *RootModel - selectionList list.Model - listSize int - input string - output *string -} - -type originStopItem struct { - title string - id string -} - -func (sI originStopItem) Title() string { - return sI.title -} - -func (sI originStopItem) Description() string { - return sI.id -} - -func (sI originStopItem) FilterValue() string { - return sI.title -} - -func (s *originSelectState) Update(msg tea.Msg) (AppState, tea.Cmd){ - var cmd tea.Cmd - - s.selectionList, cmd = s.selectionList.Update(msg) - - switch msg := msg.(type) { - case tea.KeyMsg: - if msg.Type == tea.KeyEnter { - // create and push next state IF there are stops - if s.listSize == 0 { - s.root.States.Pop() - return s, cmd - } - - selectedItem := s.selectionList.SelectedItem() - - selectedID := selectedItem.(originStopItem).id - log.Debug("stop selected", "id", selectedID) - s.root.OriginID = selectedID - - s.root.States.Push(newDestInputState(s.root)) - - return s, cmd - } - - } - - return s, cmd -} - -// updates sidebar flexbox to display the selection list -func (s *originSelectState) RenderCells(f *flexbox.FlexBox) { - prompt := "Select stop:\n" - - // TODO: better way to store these values? - sidebarHeight := s.root.Sidebar.GetHeight() - sidebarWidth := s.root.Sidebar.GetWidth() - - s.selectionList.SetSize(sidebarWidth - 7, sidebarHeight - 10) - - sidebar := styles.WelcomeSidebarContent.Render(styles.Prompt.Render(prompt) + s.selectionList.View()) - - f.GetRow(0).GetCell(1). - SetContent(sidebar). - SetStyle(styles.WelcomeSidebar) -} - -// creates a new selection state which can then -// be pushed onto states -func newOriginSelectState(root *RootModel, input string) AppState { - sl := list.New([]list.Item{}, list.NewDefaultDelegate(), 20, 10) - sl.SetShowTitle(false) - sl.SetShowHelp(false) - sl.SetShowStatusBar(false) - - m := &originSelectState{ - selectionList: sl, - root: root, - input: input, - } - - originSelectStop(m) - - return m -} - -// gets the stops which match the input string, formats them into a selection list -func originSelectStop(m *originSelectState) { - stops := m.root.Client.FindStop(m.input) - - m.listSize = len(stops) - if m.listSize == 0 { - log.Debug("No stops found") - } - - listItems := make([]list.Item, len(stops)) - for i, stop := range stops { - listItems[i] = originStopItem{ title: stop.Name, id: stop.ID } - } - - m.selectionList.SetItems(listItems) - m.selectionList.Select(0) -} - diff --git a/ui/render_route.go b/ui/render_route.go deleted file mode 100644 index af4d485..0000000 --- a/ui/render_route.go +++ /dev/null @@ -1,85 +0,0 @@ -package ui - -import ( - "github.com/charmbracelet/lipgloss" - "github.com/isobelmcrae/trip/api" - "github.com/isobelmcrae/trip/rendermaps" - "github.com/isobelmcrae/trip/styles" -) - -func (s *routeState) renderLeg(legs []api.Leg, legIdx int) { - l := legs[legIdx] - - width, height := s.root.Main.GetWidth(), s.root.Main.GetHeight() - - // focus on the leg's origin and destination - centerLat, centerLon, zoom := rendermaps.FocusOn( - l.Origin.Coord[0], l.Origin.Coord[1], - l.Destination.Coord[0], l.Destination.Coord[1], - width, height, - ) - - // we don't wanna zoom in further than this - if zoom > 14 { - zoom = 14 - } - - renderer := rendermaps.RenderMap(width - 4, height - 2, centerLat, centerLon, zoom) - renderer.Draw([]string{"landuse", "water", "building", "road", "admin"}) - - // draw the leg as a red line - for leg := range legs { - // TODO(iso): make this actually do the colours of the transport type - // this is just testing data for now - - var hex string - - hex = styles.HexColourForLine(legs[leg].Transportation.DisassembledName) - - l := legs[leg] - renderPartLeg(s, renderer, l, centerLat, centerLon, zoom, hex) - } - - // but still draw the rest of the lines too - renderer.Draw([]string{"place_label", "poi_label"}) - - frame := renderer.Frame() - - s.root.Main.SetContent( - lipgloss.JoinVertical(lipgloss.Right, lipgloss.JoinHorizontal(lipgloss.Center, frame)), - ) -} - -func renderPartLeg( - s *routeState, - renderer *rendermaps.Renderer, l api.Leg, - centerLat float64, centerLon float64, zoom float64, hex string, -) { - // no more naive method - /* renderer.Canvas.SplatLineGeo( - l.Origin.Coord[0], l.Origin.Coord[1], - l.Destination.Coord[0], l.Destination.Coord[1], - centerLat, centerLon, - zoom, hex, - ) */ - - for i := 1; i < len(l.StopSequence); i++ { - stop := l.StopSequence[i].ID - prevStop := l.StopSequence[i-1].ID - - points, err := s.root.Client.GetJourneyLeg(prevStop, stop) - if err != nil { - continue // skip this leg if we can't find a path - } - - pointsLen := len(points) - for j := 0; j < pointsLen-1; j++ { - renderer.Canvas.SplatLineGeo( - points[j][0], points[j][1], - points[j+1][0], points[j+1][1], - centerLat, centerLon, - zoom, hex, - ) - } - } -} diff --git a/ui/root.go b/ui/root.go deleted file mode 100644 index ec27fc4..0000000 --- a/ui/root.go +++ /dev/null @@ -1,114 +0,0 @@ -package ui - -import ( - "database/sql" - "github.com/76creates/stickers/flexbox" - tea "github.com/charmbracelet/bubbletea" - "github.com/isobelmcrae/trip/styles" - "github.com/isobelmcrae/trip/api" - "github.com/isobelmcrae/trip/state" - "github.com/charmbracelet/log" - _ "github.com/mattn/go-sqlite3" -) - -type RootModel struct { - // each "state" modifies the content inside of - // one of the flexbox cells - flexBox *flexbox.FlexBox - - States StateStack - - Client *api.TripClient - - OriginID string - DestinationID string - - Sidebar *flexbox.Cell - Main *flexbox.Cell -} - -func InitialiseRootModel() (m *RootModel){ - // figure out what to do with this + other strings - var welcome = "trip v0.0.1\n\nsydney public transport for your terminal\n\nhjkl/arrow keys to move\nesc to go back, enter to select\nctrl+c to exit" - - // create base flexbox cells - m = &RootModel { - flexBox: flexbox.New(0,0), - } - - rows := []*flexbox.Row{ - m.flexBox.NewRow().AddCells( - flexbox.NewCell(7, 1).SetStyle(styles.Border), - flexbox.NewCell(3, 1).SetStyle(styles.Border), - ), - } - - m.flexBox.AddRows(rows) - - m.Sidebar = m.flexBox.GetRow(0).GetCell(1) - m.Main = m.flexBox.GetRow(0).GetCell(0) - - db, err := sql.Open("sqlite3", state.DatabasePath) - if err != nil { - log.Fatal(err) - } - // defer db.Close() - m.Client = api.NewClient(db) - - m.States.Push(newOriginInputState(m)) - - main := styles.WelcomeMainContent.Render(welcome) - m.flexBox.GetRow(0).GetCell(0).SetContent(main). - SetStyle(styles.WelcomeMain) - - return m -} - -func (m *RootModel) Init() tea.Cmd { - return tea.Batch( - tea.SetWindowTitle("trip"), - ) -} - -func (m *RootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.flexBox.SetWidth(msg.Width) - m.flexBox.SetHeight(msg.Height) - case tea.KeyMsg: - switch msg.Type { - case tea.KeyCtrlC: - return m, tea.Quit - case tea.KeyEsc: - m.States.Pop() - return m, cmd - } - } - - current := m.States.Peek() - if current == nil { - return m, nil - } - - oldStateSize := m.States.Size() - updatedState, cmd := current.Update(msg) - newStateSize := m.States.Size() - - if oldStateSize == newStateSize { - m.States.states[len(m.States.states) - 1] = updatedState - } - - m.View() - - return m, cmd -} - -func (m *RootModel) View() string { - state := m.States.Peek() - if state != nil { - state.RenderCells(m.flexBox) - } - return m.flexBox.Render() -} diff --git a/ui/route.go b/ui/route.go deleted file mode 100644 index 3eb3dfa..0000000 --- a/ui/route.go +++ /dev/null @@ -1,450 +0,0 @@ -package ui - -import ( - "context" - "fmt" - "math" - "os" - "strings" - "time" - - "github.com/76creates/stickers/flexbox" - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/paginator" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/log" - "github.com/isobelmcrae/trip/api" - "github.com/isobelmcrae/trip/styles" -) - -type legSelectionKeymap struct { - PrevLeg key.Binding - NextLeg key.Binding -} - -// up to move up, down to move down -var legSelectionKeymapDefault = legSelectionKeymap{ - PrevLeg: key.NewBinding(key.WithKeys("up", "k")), - NextLeg: key.NewBinding(key.WithKeys("down", "j")), -} - -// routeState holds the state for the route view. -type routeState struct { - root *RootModel - Routes []api.Journey - paginator paginator.Model - viewport viewport.Model - legWidth int - loc *time.Location - legSelection int - legOffsets []int // Track vertical positions of each leg - legHeights []int // Track actual heights of each leg - - // Smooth scrolling state - targetYOffset int - isScrolling bool - smoothScrolling bool // Whether smooth scrolling is enabled -} - -// getRoutes fetches trip plans from the API. -func (s *routeState) getRoutes() []api.Journey { - // TODO: handle req which take a long time - routes, err := s.root.Client.TripPlan(context.TODO(), s.root.OriginID, s.root.DestinationID) - if err != nil { - log.Debug("Error when fetching routes", "err", err) - } - - if routes != nil { - log.Debug("routes found", "count", len(routes)) - } - - return routes -} - -// newRouteState initializes the state for the route view. -func newRouteState(root *RootModel) AppState { - location, _ := time.LoadLocation("Australia/Sydney") - - // Check if smooth scrolling should be enabled - // Disable for SSH connections or when explicitly disabled - smoothScrolling := true - if os.Getenv("SSH_CONNECTION") != "" || os.Getenv("SSH_CLIENT") != "" || os.Getenv("SSH_TTY") != "" { - smoothScrolling = false - } - if os.Getenv("TRIP_SMOOTH_SCROLL") == "false" || os.Getenv("TRIP_SMOOTH_SCROLL") == "0" { - smoothScrolling = false - } - if os.Getenv("TRIP_SMOOTH_SCROLL") == "true" || os.Getenv("TRIP_SMOOTH_SCROLL") == "1" { - smoothScrolling = true - } - - s := &routeState{ - root: root, - loc: location, - legSelection: 0, - targetYOffset: 0, - isScrolling: false, - smoothScrolling: smoothScrolling, - } - - originalRoutes := s.getRoutes() - - // Filter routes to only include future journeys. - now := time.Now() - for _, route := range originalRoutes { - if len(route.Legs) > 0 { - routeStartTime := route.Legs[0].Origin.DepartureTimeEstimated - parsedTime, err := time.Parse(time.RFC3339, routeStartTime) - if err == nil && parsedTime.After(now) { - s.Routes = append(s.Routes, route) - } - } - } - - // measurements are relative to root's flexbox - bigWidth := s.root.flexBox.GetWidth() - width := int(math.Floor(float64(bigWidth)/10)*3) - 6 - height := s.root.flexBox.GetHeight() - 6 - - // Update our leg width and the viewport's dimensions. - s.legWidth = width - 2 - s.viewport.Width = width - s.viewport.Height = height - - // Initialise the viewport. - s.viewport = viewport.New(width, height) - - // Initialise the paginator. - s.paginator = paginator.New() - s.paginator.Type = paginator.Dots - s.paginator.PerPage = 1 - s.paginator.ActiveDot = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "235", Dark: "252"}).PaddingRight(1).Render("⬤") - s.paginator.InactiveDot = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "250", Dark: "238"}).PaddingRight(1).Render("⬤") - s.paginator.SetTotalPages(len(s.Routes)) - - // Set initial content, handling the no-routes case. - if len(s.Routes) == 0 { - s.viewport.SetContent(lipgloss.NewStyle().Width(s.legWidth).Align(lipgloss.Center).Render("No routes found.")) - } else { - s.setViewportContent(0) - } - - return s -} - -// setViewportContent sets the viewport content and calculates leg offsets -func (s *routeState) setViewportContent(routeIndex int) { - if routeIndex >= len(s.Routes) { - return - } - - content, offsets, heights := s.displayRouteWithOffsetsAndHeights(s.Routes[routeIndex]) - s.legOffsets = offsets - s.legHeights = heights - s.viewport.SetContent(content) -} - -// smoothScrollTo initiates smooth scrolling to a target Y offset -func (s *routeState) smoothScrollTo(targetOffset int) tea.Cmd { - if targetOffset < 0 { - targetOffset = 0 - } - - s.targetYOffset = targetOffset - s.isScrolling = true - - return tea.Tick(time.Millisecond*16, func(time.Time) tea.Msg { - return smoothScrollMsg{} - }) -} - -type smoothScrollMsg struct{} - -// performSmoothScrollStep performs one step of smooth scrolling -func (s *routeState) performSmoothScrollStep() tea.Cmd { - if !s.isScrolling { - return nil - } - - currentOffset := s.viewport.YOffset - diff := s.targetYOffset - currentOffset - - // If we're close enough, snap to target - if abs(diff) <= 1 { - s.viewport.SetYOffset(s.targetYOffset) - s.isScrolling = false - return nil - } - - // Move 20% of the remaining distance each step - step := diff / 5 - if step == 0 { - if diff > 0 { - step = 1 - } else { - step = -1 - } - } - - newOffset := currentOffset + step - if newOffset < 0 { - newOffset = 0 - } - - s.viewport.SetYOffset(newOffset) - - // Continue smooth scrolling - return tea.Tick(time.Millisecond*16, func(time.Time) tea.Msg { - return smoothScrollMsg{} - }) -} - -func abs(x int) int { - if x < 0 { - return -x - } - return x -} - -// scrollToSelectedLeg scrolls the viewport to keep the selected leg visible -func (s *routeState) scrollToSelectedLeg() tea.Cmd { - if s.legSelection >= len(s.legOffsets) || len(s.legOffsets) == 0 { - return nil - } - - selectedLegOffset := s.legOffsets[s.legSelection] - selectedLegHeight := s.legHeights[s.legSelection] - viewportTop := s.viewport.YOffset - viewportBottom := viewportTop + s.viewport.Height - - var targetOffset int - needsScroll := false - - // Special case: if first leg is selected, scroll to top to show header - if s.legSelection == 0 { - targetOffset = 0 - needsScroll = true - } else if selectedLegOffset < viewportTop { - // If selected leg is above the viewport, scroll up to show it - targetOffset = selectedLegOffset - needsScroll = true - } else if selectedLegOffset+selectedLegHeight > viewportBottom { - // If selected leg is below the viewport, scroll down to show it - targetOffset = selectedLegOffset + selectedLegHeight - s.viewport.Height - if targetOffset < 0 { - targetOffset = 0 - } - needsScroll = true - } - - if needsScroll { - if s.smoothScrolling { - return s.smoothScrollTo(targetOffset) - } else { - s.viewport.SetYOffset(targetOffset) - } - } - - return nil -} - -// displayRouteWithOffsetsAndHeights formats the details of a single journey and tracks leg positions and heights -func (s *routeState) displayRouteWithOffsetsAndHeights(r api.Journey) (string, []int, []int) { - var doc strings.Builder - var offsets []int - var heights []int - - if len(r.Legs) == 0 { - return "This journey has no legs.", []int{}, []int{} - } - - origin := r.Legs[0].Origin - destination := r.Legs[len(r.Legs)-1].Destination - - // Wrap the title text to fit within the leg width - originText := fmt.Sprintf("%s @%s", origin.DisassembledName, formatTime(s.loc, origin.DepartureTimeEstimated)) - destText := fmt.Sprintf("%s @%s", destination.DisassembledName, formatTime(s.loc, destination.ArrivalTimeEstimated)) - - wrappedOrigin := lipgloss.NewStyle().Width(s.legWidth).Render(originText) - wrappedDest := lipgloss.NewStyle().Width(s.legWidth).Render(destText) - - title := fmt.Sprintf("%s\n\n%s\n\n", wrappedOrigin, wrappedDest) - doc.WriteString(title) - - // Count lines in title for offset calculation - titleLines := strings.Count(title, "\n") - currentOffset := titleLines - - for idx, leg := range r.Legs { - offsets = append(offsets, currentOffset) - legContent := s.formatLeg(leg, idx) - doc.WriteString(legContent) - - // Calculate actual height of this leg by counting newlines in the rendered content - legHeight := strings.Count(legContent, "\n") - heights = append(heights, legHeight) - currentOffset += legHeight - } - - return doc.String(), offsets, heights -} - -// formatTime converts a time string to a readable format. -func formatTime(loc *time.Location, rawTime string) string { - if rawTime == "" { - return "n/a" - } - parsed, err := time.Parse(time.RFC3339, rawTime) - if err != nil { - return "invalid time" - } - return parsed.In(loc).Format("3:04pm") -} - -// formatLeg formats the display for a single leg of a journey. -func (s *routeState) formatLeg(l api.Leg, idx int) string { - var transport string - if l.Transportation.DisassembledName == "" { - transport = "WALK" - } else { - transport = l.Transportation.DisassembledName - } - - lineStr := styles.CreateLineHighlight(transport).Render(fmt.Sprintf("[%s]", transport)) - originStr := fmt.Sprintf("%s %s | %s", lineStr, l.Origin.DisassembledName, formatTime(s.loc, l.Origin.DepartureTimeEstimated)) - destStr := fmt.Sprintf("%s %s | %s", lineStr, l.Destination.DisassembledName, formatTime(s.loc, l.Destination.ArrivalTimeEstimated)) - duration := l.Duration / 60 - - var showSelectedStr string - isSelected := idx == s.legSelection - if isSelected { - showSelectedStr = " (focused)" - } - - // Add position labels for start and end legs - var positionLabel string - if len(s.Routes) > 0 && s.paginator.Page < len(s.Routes) { - totalLegs := len(s.Routes[s.paginator.Page].Legs) - if idx == 0 { - positionLabel = " [START]" - } else if idx == totalLegs-1 { - positionLabel = " [END]" - } - } - - leg := fmt.Sprintf("%s\n\n> Travel for %dmin%s%s\n\n%s", originStr, duration, showSelectedStr, positionLabel, destStr) - - return styles.FormatRouteLeg(s.legWidth, transport, isSelected).Render(leg) + "\n" -} - -// RenderCells renders the viewport and paginator into the flexbox layout. -func (s *routeState) RenderCells(f *flexbox.FlexBox) { - var finalView string - - if len(s.Routes) > 0 { - // arrowedPaginator := lipgloss.JoinHorizontal(lipgloss.Left, "◄ ", s.paginator.View(), " ►") - // styledPaginator := lipgloss.NewStyle().Width(s.legWidth).Align(lipgloss.Center).Render(arrowedPaginator) - styledPaginator := lipgloss.NewStyle().Width(s.legWidth).Align(lipgloss.Center).Render(s.paginator.View()) - finalView = lipgloss.JoinVertical(lipgloss.Left, s.viewport.View(), "\n", styledPaginator) - } else { - finalView = s.viewport.View() - } - - s.root.Sidebar.SetContent(finalView) - - // TODO render map here - if len(s.Routes) > 0 && s.paginator.Page < len(s.Routes) { - s.renderLeg(s.Routes[s.paginator.Page].Legs, s.legSelection) - } -} - -// Update handles messages and updates the state. -func (s *routeState) Update(msg tea.Msg) (AppState, tea.Cmd) { - var cmds []tea.Cmd - var paginatorCmd, viewportCmd tea.Cmd - - pageBefore := s.paginator.Page - legSelectionBefore := s.legSelection - - switch msg := msg.(type) { - case smoothScrollMsg: - // Handle smooth scrolling animation only if smooth scrolling is enabled - if s.smoothScrolling { - if cmd := s.performSmoothScrollStep(); cmd != nil { - cmds = append(cmds, cmd) - } - } - - case tea.WindowSizeMsg: - // Get new dimensions - bigWidth := s.root.flexBox.GetWidth() - width := int(math.Floor(float64(bigWidth)/10)*3) - 6 - height := s.root.flexBox.GetHeight() - 6 - - // Update dimensions - s.legWidth = width - 2 - s.viewport.Width = width - s.viewport.Height = height - - // Re-wrap the content in the viewport with the new width. - if len(s.Routes) > 0 { - s.setViewportContent(s.paginator.Page) - // After resizing, ensure the selected leg is still visible - if cmd := s.scrollToSelectedLeg(); cmd != nil { - cmds = append(cmds, cmd) - } - } else { - s.viewport.SetContent(lipgloss.NewStyle().Width(s.legWidth).Align(lipgloss.Center).Render("No routes found.")) - } - s.RenderCells(s.root.flexBox) - return s, tea.Batch(cmds...) - - case tea.KeyMsg: - // Handle leg selection keys first - switch { - case key.Matches(msg, legSelectionKeymapDefault.NextLeg): - if len(s.Routes) > 0 && s.paginator.Page < len(s.Routes) { - maxLeg := len(s.Routes[s.paginator.Page].Legs) - 1 - if s.legSelection < maxLeg { - s.legSelection++ - } - } - case key.Matches(msg, legSelectionKeymapDefault.PrevLeg): - if s.legSelection > 0 { - s.legSelection-- - } - default: - // For pagination and viewport scrolling (left/right arrows, page up/down) - if len(s.Routes) > 0 { - s.paginator, paginatorCmd = s.paginator.Update(msg) - s.viewport, viewportCmd = s.viewport.Update(msg) - cmds = append(cmds, paginatorCmd, viewportCmd) - } - } - default: - // For all other messages, pass them to the children. - if len(s.Routes) > 0 { - s.paginator, paginatorCmd = s.paginator.Update(msg) - s.viewport, viewportCmd = s.viewport.Update(msg) - cmds = append(cmds, paginatorCmd, viewportCmd) - } - } - - // After any potential update, check if the page has changed. - if len(s.Routes) > 0 && s.paginator.Page != pageBefore { - s.legSelection = 0 // Reset leg selection when changing routes - s.setViewportContent(s.paginator.Page) - s.viewport.GotoTop() - } else if len(s.Routes) > 0 && s.legSelection != legSelectionBefore { - // Leg selection changed, update content and scroll to selected leg - s.setViewportContent(s.paginator.Page) - if cmd := s.scrollToSelectedLeg(); cmd != nil { - cmds = append(cmds, cmd) - } - } - - s.RenderCells(s.root.flexBox) - return s, tea.Batch(cmds...) -} diff --git a/ui/state.go b/ui/state.go deleted file mode 100644 index e5257e2..0000000 --- a/ui/state.go +++ /dev/null @@ -1,51 +0,0 @@ -// staacccccck -package ui - -import ( - "github.com/76creates/stickers/flexbox" - tea "github.com/charmbracelet/bubbletea" -) - -type StateStack struct { - states []AppState -} - -// A view of the app -type AppState interface { - Update(msg tea.Msg) (AppState, tea.Cmd) - RenderCells(*flexbox.FlexBox) -} - -// Set state as the current app state -func (s *StateStack) Push(state AppState) { - s.states = append(s.states, state) -} - -// "Go back" - set the previous state of the app -// as the current -// Prevents user from showing a state before the welcome -func (s *StateStack) Pop() AppState { - if len(s.states) == 1 { - return nil - } - - top := s.states[len(s.states) - 1] - s.states = s.states[:len(s.states) - 1] - - return top -} - -// See the current state of the app -func (s *StateStack) Peek() AppState { - if len(s.states) == 0 { - return nil - } - - return s.states[len(s.states) - 1] -} - -// see how many states currently are on the stack -func (s *StateStack) Size() int { - return len(s.states) -} -