From 0bd687b7e868ee6dd0c22c14e9aeef73acd20472 Mon Sep 17 00:00:00 2001 From: isobelmcrae Date: Thu, 18 Dec 2025 00:37:27 +1100 Subject: [PATCH 1/4] fix(rendermaps/cache): module no longer creates dir on import --- main.go | 21 ++++++++++++++++++--- rendermaps/cache.go | 38 +++++++++++++++++++++++++++++++------- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/main.go b/main.go index abc32ce..4db518e 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "flag" "os" "os/signal" + "path/filepath" "syscall" "time" @@ -27,17 +28,31 @@ const ( func main() { err := godotenv.Load() if err != nil { - log.Error("Error loading .env") + log.Error("Error loading .env", err) } + + /* TODO: convert to viper for flags & config */ 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) + /* configure logging to file in user config directory */ + /* TODO: change log directory? */ + confDir, err := os.UserConfigDir() if err != nil { panic(err) } + + tripConfigDir := filepath.Join(confDir, "trip") + if err := os.MkdirAll(tripConfigDir, 0755); err != nil { + panic(err) + } + + f, err := os.OpenFile(filepath.Join(tripConfigDir, "trip.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); + if err != nil { + panic(err) + } + log.SetOutput(f) log.SetLevel(log.DebugLevel) log.SetReportCaller(true) diff --git a/rendermaps/cache.go b/rendermaps/cache.go index 83f0346..25b9aa1 100644 --- a/rendermaps/cache.go +++ b/rendermaps/cache.go @@ -5,19 +5,38 @@ import ( "path/filepath" ) -var ( - localCache = createCache() -) +var localCache string -func createCache() string { - cache := filepath.Join(os.Getenv("HOME"), ".cache", "trip") +func createCache() error { + userCache, err := os.UserCacheDir() + if err != nil { + return err + } + + cache := filepath.Join(userCache, "trip") if err := os.MkdirAll(cache, 0755); err != nil { - panic(err) + return err + } + + localCache = cache + return nil +} + +/* used to prevent creating files on import */ +func ensureCache() error { + if localCache != "" { + return nil } - return cache + + return createCache() } func cacheInsertKey(key string, value []byte) { + err := ensureCache() + if err != nil { + panic(err) + } + cacheFile := filepath.Join(localCache, key) if err := os.WriteFile(cacheFile, value, 0644); err != nil { panic(err) @@ -25,6 +44,11 @@ func cacheInsertKey(key string, value []byte) { } func cacheGetKey(key string) ([]byte, error) { + err := ensureCache() + if err != nil { + panic(err) + } + cacheFile := filepath.Join(localCache, key) data, err := os.ReadFile(cacheFile) if err != nil { From 118c7744e93713ae854c7aeee36521a5913193d3 Mon Sep 17 00:00:00 2001 From: isobelmcrae Date: Thu, 18 Dec 2025 00:50:23 +1100 Subject: [PATCH 2/4] chore: add gomod2nix --- default.nix | 22 ++++++++ flake.lock | 85 ++++++++++++++++++++++++++++ flake.nix | 74 +++++++++++++++++++++++++ gomod2nix.toml | 147 +++++++++++++++++++++++++++++++++++++++++++++++++ shell.nix | 25 +++++++++ 5 files changed, 353 insertions(+) create mode 100644 default.nix create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 gomod2nix.toml create mode 100644 shell.nix diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..2a57c2a --- /dev/null +++ b/default.nix @@ -0,0 +1,22 @@ +{ + pkgs ? ( + let + inherit (builtins) fetchTree fromJSON readFile; + inherit ((fromJSON (readFile ./flake.lock)).nodes) nixpkgs gomod2nix; + in + import (fetchTree nixpkgs.locked) { + overlays = [ + (import "${fetchTree gomod2nix.locked}/overlay.nix") + ]; + } + ), + buildGoApplication ? pkgs.buildGoApplication, +}: + +buildGoApplication { + pname = "trip"; + version = "0.1"; + pwd = ./.; + src = ./.; + modules = ./gomod2nix.toml; +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..e78b24c --- /dev/null +++ b/flake.lock @@ -0,0 +1,85 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "gomod2nix": { + "inputs": { + "flake-utils": [ + "flake-utils" + ], + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1763982521, + "narHash": "sha256-ur4QIAHwgFc0vXiaxn5No/FuZicxBr2p0gmT54xZkUQ=", + "owner": "nix-community", + "repo": "gomod2nix", + "rev": "02e63a239d6eabd595db56852535992c898eba72", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "gomod2nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1765779637, + "narHash": "sha256-KJ2wa/BLSrTqDjbfyNx70ov/HdgNBCBBSQP3BIzKnv4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "1306659b587dc277866c7b69eb97e5f07864d8c4", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "gomod2nix": "gomod2nix", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..a10b8dc --- /dev/null +++ b/flake.nix @@ -0,0 +1,74 @@ +{ + description = "A basic gomod2nix flake"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + inputs.flake-utils.url = "github:numtide/flake-utils"; + inputs.gomod2nix.url = "github:nix-community/gomod2nix"; + inputs.gomod2nix.inputs.nixpkgs.follows = "nixpkgs"; + inputs.gomod2nix.inputs.flake-utils.follows = "flake-utils"; + + outputs = + { + self, + nixpkgs, + flake-utils, + gomod2nix, + }: + (flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + + callPackage = pkgs.callPackage; + # Simple test check added to nix flake check + go-test = pkgs.stdenvNoCC.mkDerivation { + name = "go-test"; + dontBuild = true; + src = ./.; + doCheck = true; + nativeBuildInputs = with pkgs; [ + go + writableTmpDirAsHomeHook + ]; + preBuild = '' + export HOME=$(mktemp -d) + ''; + checkPhase = '' + go test -v ./... + ''; + installPhase = '' + mkdir "$out" + ''; + }; + # Simple lint check added to nix flake check + go-lint = pkgs.stdenvNoCC.mkDerivation { + name = "go-lint"; + dontBuild = true; + src = ./.; + doCheck = true; + nativeBuildInputs = with pkgs; [ + golangci-lint + go + writableTmpDirAsHomeHook + ]; + checkPhase = '' + golangci-lint run + ''; + installPhase = '' + mkdir "$out" + ''; + }; + in + { + checks = { + inherit go-test go-lint; + }; + packages.default = callPackage ./. { + inherit (gomod2nix.legacyPackages.${system}) buildGoApplication; + }; + devShells.default = callPackage ./shell.nix { + inherit (gomod2nix.legacyPackages.${system}) mkGoEnv gomod2nix; + }; + } + )); +} diff --git a/gomod2nix.toml b/gomod2nix.toml new file mode 100644 index 0000000..4549680 --- /dev/null +++ b/gomod2nix.toml @@ -0,0 +1,147 @@ +schema = 3 + +[mod] + [mod."github.com/76creates/stickers"] + version = "v1.4.1" + hash = "sha256-nUvY5MyiIXk4PbkLyx14b2yryjCjUedyy2un/RYnksw=" + [mod."github.com/anmitsu/go-shlex"] + version = "v0.0.0-20200514113438-38f4b401e2be" + hash = "sha256-L3Ak4X2z7WXq7vMKuiHCOJ29nlpajUQ08Sfb9T0yP54=" + [mod."github.com/atotto/clipboard"] + version = "v0.1.4" + hash = "sha256-ZZ7U5X0gWOu8zcjZcWbcpzGOGdycwq0TjTFh/eZHjXk=" + [mod."github.com/aymanbagabas/go-osc52/v2"] + version = "v2.0.1" + hash = "sha256-6Bp0jBZ6npvsYcKZGHHIUSVSTAMEyieweAX2YAKDjjg=" + [mod."github.com/charmbracelet/bubbles"] + version = "v0.21.0" + hash = "sha256-cfjUHgy9eq5SretTHuYuRaeeT6QmJYQBB9dsI8QSnW0=" + [mod."github.com/charmbracelet/bubbletea"] + version = "v1.3.6" + hash = "sha256-79MpQk+92RDXnNc2hSct8EDf0AhV6MZpvFMmmrVC/bs=" + [mod."github.com/charmbracelet/colorprofile"] + version = "v0.2.3-0.20250311203215-f60798e515dc" + hash = "sha256-D9E/bMOyLXAUVOHA1/6o3i+vVmLfwIMOWib6sU7A6+Q=" + [mod."github.com/charmbracelet/keygen"] + version = "v0.5.3" + hash = "sha256-VUVU4BSanfpoKlESxjd8jm/pKBvekDnjP3mUqULjd+g=" + [mod."github.com/charmbracelet/lipgloss"] + version = "v1.1.0" + hash = "sha256-RHsRT2EZ1nDOElxAK+6/DC9XAaGVjDTgPvRh3pyCfY4=" + [mod."github.com/charmbracelet/log"] + version = "v0.4.2" + hash = "sha256-3w1PCM/c4JvVEh2d0sMfv4C77Xs1bPa1Ea84zdynC7I=" + [mod."github.com/charmbracelet/ssh"] + version = "v0.0.0-20250429213052-383d50896132" + hash = "sha256-vVJ6u0KVGW25uD39/IwgjCcWi/k/3DDIvpnRYb7j8zk=" + [mod."github.com/charmbracelet/wish"] + version = "v1.4.7" + hash = "sha256-wNg6APevKC8pi7aAt9fc81Wad1rMy/BgzVAsAu9Vo3U=" + [mod."github.com/charmbracelet/x/ansi"] + version = "v0.9.3" + hash = "sha256-Bzum17p7UQZeNxL155Pho/+GXj1DElB9Bp3O194CYf8=" + [mod."github.com/charmbracelet/x/cellbuf"] + version = "v0.0.13-0.20250311204145-2c3ea96c31dd" + hash = "sha256-XAhCOt8qJ2vR77lH1ez0IVU1/2CaLTq9jSmrHVg5HHU=" + [mod."github.com/charmbracelet/x/conpty"] + version = "v0.1.0" + hash = "sha256-VvqJl1WVm7ozO+Dmgl4e/iM9Z9FVrQLrLuA0Sr50KAM=" + [mod."github.com/charmbracelet/x/errors"] + version = "v0.0.0-20240508181413-e8d8b6e2de86" + hash = "sha256-GO8hf0lhVtl00C+xoTzvBtPU2cO0PymSLc2szBRUNtE=" + [mod."github.com/charmbracelet/x/input"] + version = "v0.3.4" + hash = "sha256-QdpH3Pfu04qMdQEnT80bJRJZIfMe4Cw8/HbM2OETZdM=" + [mod."github.com/charmbracelet/x/term"] + version = "v0.2.1" + hash = "sha256-VBkCZLI90PhMasftGw3403IqoV7d3E5WEGAIVrN5xQM=" + [mod."github.com/charmbracelet/x/termios"] + version = "v0.1.0" + hash = "sha256-cvyxWoAf3gDv17Bupd6X4fRkPPF87QU8F4vmsFw97QA=" + [mod."github.com/charmbracelet/x/windows"] + version = "v0.2.0" + hash = "sha256-pDAd1E5w66E/d3vuTyzgnW+W/KegZ2sxQQMfoEn7S1A=" + [mod."github.com/creack/pty"] + version = "v1.1.21" + hash = "sha256-pjGw6wQlrVhN65XaIxZueNJqnXThGu00u24rKOLzxS0=" + [mod."github.com/erikgeiser/coninput"] + version = "v0.0.0-20211004153227-1c3628e74d0f" + hash = "sha256-OWSqN1+IoL73rWXWdbbcahZu8n2al90Y3eT5Z0vgHvU=" + [mod."github.com/flywave/go-earcut"] + version = "v0.0.0-20210712015426-7084f78cceb3" + hash = "sha256-nhnbHWvvl5RD7DaQT+RNyl80AV/7vBGebtbPXbR0tso=" + [mod."github.com/go-logfmt/logfmt"] + version = "v0.6.0" + hash = "sha256-RtIG2qARd5sT10WQ7F3LR8YJhS8exs+KiuUiVf75bWg=" + [mod."github.com/gogo/protobuf"] + version = "v1.3.2" + hash = "sha256-pogILFrrk+cAtb0ulqn9+gRZJ7sGnnLLdtqITvxvG6c=" + [mod."github.com/google/go-querystring"] + version = "v1.1.0" + hash = "sha256-itsKgKghuX26czU79cK6C2n+lc27jm5Dw1XbIRgwZJY=" + [mod."github.com/joho/godotenv"] + version = "v1.5.1" + hash = "sha256-kA0osKfsc6Kp+nuGTRJyXZZlJt1D/kuEazKMWYCWcQ8=" + [mod."github.com/lucasb-eyer/go-colorful"] + version = "v1.2.0" + hash = "sha256-Gg9dDJFCTaHrKHRR1SrJgZ8fWieJkybljybkI9x0gyE=" + [mod."github.com/mattn/go-isatty"] + version = "v0.0.20" + hash = "sha256-qhw9hWtU5wnyFyuMbKx+7RB8ckQaFQ8D+8GKPkN3HHQ=" + [mod."github.com/mattn/go-localereader"] + version = "v0.0.1" + hash = "sha256-JlWckeGaWG+bXK8l8WEdZqmSiTwCA8b1qbmBKa/Fj3E=" + [mod."github.com/mattn/go-runewidth"] + version = "v0.0.16" + hash = "sha256-NC+ntvwIpqDNmXb7aixcg09il80ygq6JAnW0Gb5b/DQ=" + [mod."github.com/mattn/go-sqlite3"] + version = "v1.14.28" + hash = "sha256-mskU1xki6J1Fj6ItNgY/XNetB4Ta4jufEr4+JvTd7qs=" + [mod."github.com/muesli/ansi"] + version = "v0.0.0-20230316100256-276c6243b2f6" + hash = "sha256-qRKn0Bh2yvP0QxeEMeZe11Vz0BPFIkVcleKsPeybKMs=" + [mod."github.com/muesli/cancelreader"] + version = "v0.2.2" + hash = "sha256-uEPpzwRJBJsQWBw6M71FDfgJuR7n55d/7IV8MO+rpwQ=" + [mod."github.com/muesli/termenv"] + version = "v0.16.0" + hash = "sha256-hGo275DJlyLtcifSLpWnk8jardOksdeX9lH4lBeE3gI=" + [mod."github.com/paulmach/orb"] + version = "v0.11.1" + hash = "sha256-oh5J5jl15FZwNm0jiYvPxNprMbufuSOELtvYHk1LLNs=" + [mod."github.com/paulmach/protoscan"] + version = "v0.2.1" + hash = "sha256-K5BOkSti6rgMofgdH72FgvFX0++KZsYI+QqKHZg2S5c=" + [mod."github.com/rivo/uniseg"] + version = "v0.4.7" + hash = "sha256-rDcdNYH6ZD8KouyyiZCUEy8JrjOQoAkxHBhugrfHjFo=" + [mod."github.com/sahilm/fuzzy"] + version = "v0.1.1" + hash = "sha256-f2VsDI6G+V2w31tSDzbZPi9EI2E7jRV6Aq8yeOorSZY=" + [mod."github.com/tidwall/geoindex"] + version = "v1.7.0" + hash = "sha256-Miz4xfkxm6iPE1Odw1I301RBaq4nCOW/lkLGLPYKlPA=" + [mod."github.com/tidwall/rtree"] + version = "v1.10.0" + hash = "sha256-/XdIMDlNoxS8v15pjJ25cW2xMUC7zfnywz5lezhgkwE=" + [mod."github.com/xo/terminfo"] + version = "v0.0.0-20220910002029-abceb7e1c41e" + hash = "sha256-GyCDxxMQhXA3Pi/TsWXpA8cX5akEoZV7CFx4RO3rARU=" + [mod."go.mongodb.org/mongo-driver"] + version = "v1.11.4" + hash = "sha256-Db3G9xc1JKJbscSdXKmgH5TDZylRZU5WNshqkt16z4A=" + [mod."golang.org/x/crypto"] + version = "v0.37.0" + hash = "sha256-9NwDEcii1e2JYM/+3y1yNzWnt/ChMm27e9OtfuF39OM=" + [mod."golang.org/x/exp"] + version = "v0.0.0-20240719175910-8a7402abbf56" + hash = "sha256-mHEPy0vbd/pFwq5ZAEKaehCeYVQLEFDGnXAoVgkCLPo=" + [mod."golang.org/x/sync"] + version = "v0.15.0" + hash = "sha256-Jf4ehm8H8YAWY6mM151RI5CbG7JcOFtmN0AZx4bE3UE=" + [mod."golang.org/x/sys"] + version = "v0.33.0" + hash = "sha256-wlOzIOUgAiGAtdzhW/KPl/yUVSH/lvFZfs5XOuJ9LOQ=" + [mod."golang.org/x/text"] + version = "v0.26.0" + hash = "sha256-N+27nBCyGvje0yCTlUzZoVZ0LRxx4AJ+eBlrFQVRlFQ=" diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..41d5401 --- /dev/null +++ b/shell.nix @@ -0,0 +1,25 @@ +{ + pkgs ? ( + let + inherit (builtins) fetchTree fromJSON readFile; + inherit ((fromJSON (readFile ./flake.lock)).nodes) nixpkgs gomod2nix; + in + import (fetchTree nixpkgs.locked) { + overlays = [ + (import "${fetchTree gomod2nix.locked}/overlay.nix") + ]; + } + ), + mkGoEnv ? pkgs.mkGoEnv, + gomod2nix ? pkgs.gomod2nix, +}: + +let + goEnv = mkGoEnv { pwd = ./.; }; +in +pkgs.mkShell { + packages = [ + goEnv + gomod2nix + ]; +} From fb6331b302eb0073fee77b0e55c47810ed6f7791 Mon Sep 17 00:00:00 2001 From: isobelmcrae Date: Sun, 21 Dec 2025 00:37:01 +1100 Subject: [PATCH 3/4] feat(ui): created main window, sidebar --- .gitignore | 5 + Cargo.lock | 1156 ++++++++++++++++++++++++++ Cargo.toml | 11 + api/api.go | 136 --- api/api_test/api_test.go | 9 - api/api_test/search_test.go | 65 -- api/gtfs.go | 150 ---- api/schema.sql | 41 - api/search.go | 81 -- api/types.go | 136 --- api_load_data/main.go | 160 ---- default.nix | 22 - flake.lock | 85 -- flake.nix | 74 -- go.mod | 57 -- go.sum | 184 ----- gomod2nix.toml | 147 ---- main.go | 129 --- rendermaps/cache.go | 58 -- rendermaps/canvas.go | 361 -------- rendermaps/example/main.go | 72 -- rendermaps/maps.go | 120 --- rendermaps/proj.go | 190 ----- rendermaps/style.go | 327 -------- rendermaps/style.json | 1560 ----------------------------------- rendermaps/tile.go | 221 ----- rust-toolchain.toml | 2 + shell.nix | 48 +- src/app.rs | 83 ++ src/main.rs | 13 + state/database_path.go | 19 - styles/colours.go | 201 ----- styles/flexbox.go | 30 - styles/route.go | 44 - ui/destInput.go | 56 -- ui/destSelect.go | 119 --- ui/originInput.go | 57 -- ui/originSelect.go | 121 --- ui/render_route.go | 85 -- ui/root.go | 114 --- ui/route.go | 450 ---------- ui/state.go | 51 -- 42 files changed, 1298 insertions(+), 5752 deletions(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml delete mode 100644 api/api.go delete mode 100644 api/api_test/api_test.go delete mode 100644 api/api_test/search_test.go delete mode 100644 api/gtfs.go delete mode 100644 api/schema.sql delete mode 100644 api/search.go delete mode 100644 api/types.go delete mode 100644 api_load_data/main.go delete mode 100644 default.nix delete mode 100644 flake.lock delete mode 100644 flake.nix delete mode 100644 go.mod delete mode 100644 go.sum delete mode 100644 gomod2nix.toml delete mode 100644 main.go delete mode 100644 rendermaps/cache.go delete mode 100644 rendermaps/canvas.go delete mode 100644 rendermaps/example/main.go delete mode 100644 rendermaps/maps.go delete mode 100644 rendermaps/proj.go delete mode 100644 rendermaps/style.go delete mode 100644 rendermaps/style.json delete mode 100644 rendermaps/tile.go create mode 100644 rust-toolchain.toml create mode 100644 src/app.rs create mode 100644 src/main.rs delete mode 100644 state/database_path.go delete mode 100644 styles/colours.go delete mode 100644 styles/flexbox.go delete mode 100644 styles/route.go delete mode 100644 ui/destInput.go delete mode 100644 ui/destSelect.go delete mode 100644 ui/originInput.go delete mode 100644 ui/originSelect.go delete mode 100644 ui/render_route.go delete mode 100644 ui/root.go delete mode 100644 ui/route.go delete mode 100644 ui/state.go 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/default.nix b/default.nix deleted file mode 100644 index 2a57c2a..0000000 --- a/default.nix +++ /dev/null @@ -1,22 +0,0 @@ -{ - pkgs ? ( - let - inherit (builtins) fetchTree fromJSON readFile; - inherit ((fromJSON (readFile ./flake.lock)).nodes) nixpkgs gomod2nix; - in - import (fetchTree nixpkgs.locked) { - overlays = [ - (import "${fetchTree gomod2nix.locked}/overlay.nix") - ]; - } - ), - buildGoApplication ? pkgs.buildGoApplication, -}: - -buildGoApplication { - pname = "trip"; - version = "0.1"; - pwd = ./.; - src = ./.; - modules = ./gomod2nix.toml; -} diff --git a/flake.lock b/flake.lock deleted file mode 100644 index e78b24c..0000000 --- a/flake.lock +++ /dev/null @@ -1,85 +0,0 @@ -{ - "nodes": { - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "gomod2nix": { - "inputs": { - "flake-utils": [ - "flake-utils" - ], - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1763982521, - "narHash": "sha256-ur4QIAHwgFc0vXiaxn5No/FuZicxBr2p0gmT54xZkUQ=", - "owner": "nix-community", - "repo": "gomod2nix", - "rev": "02e63a239d6eabd595db56852535992c898eba72", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "gomod2nix", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1765779637, - "narHash": "sha256-KJ2wa/BLSrTqDjbfyNx70ov/HdgNBCBBSQP3BIzKnv4=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "1306659b587dc277866c7b69eb97e5f07864d8c4", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "flake-utils": "flake-utils", - "gomod2nix": "gomod2nix", - "nixpkgs": "nixpkgs" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index a10b8dc..0000000 --- a/flake.nix +++ /dev/null @@ -1,74 +0,0 @@ -{ - description = "A basic gomod2nix flake"; - - inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - inputs.flake-utils.url = "github:numtide/flake-utils"; - inputs.gomod2nix.url = "github:nix-community/gomod2nix"; - inputs.gomod2nix.inputs.nixpkgs.follows = "nixpkgs"; - inputs.gomod2nix.inputs.flake-utils.follows = "flake-utils"; - - outputs = - { - self, - nixpkgs, - flake-utils, - gomod2nix, - }: - (flake-utils.lib.eachDefaultSystem ( - system: - let - pkgs = nixpkgs.legacyPackages.${system}; - - callPackage = pkgs.callPackage; - # Simple test check added to nix flake check - go-test = pkgs.stdenvNoCC.mkDerivation { - name = "go-test"; - dontBuild = true; - src = ./.; - doCheck = true; - nativeBuildInputs = with pkgs; [ - go - writableTmpDirAsHomeHook - ]; - preBuild = '' - export HOME=$(mktemp -d) - ''; - checkPhase = '' - go test -v ./... - ''; - installPhase = '' - mkdir "$out" - ''; - }; - # Simple lint check added to nix flake check - go-lint = pkgs.stdenvNoCC.mkDerivation { - name = "go-lint"; - dontBuild = true; - src = ./.; - doCheck = true; - nativeBuildInputs = with pkgs; [ - golangci-lint - go - writableTmpDirAsHomeHook - ]; - checkPhase = '' - golangci-lint run - ''; - installPhase = '' - mkdir "$out" - ''; - }; - in - { - checks = { - inherit go-test go-lint; - }; - packages.default = callPackage ./. { - inherit (gomod2nix.legacyPackages.${system}) buildGoApplication; - }; - devShells.default = callPackage ./shell.nix { - inherit (gomod2nix.legacyPackages.${system}) mkGoEnv gomod2nix; - }; - } - )); -} 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/gomod2nix.toml b/gomod2nix.toml deleted file mode 100644 index 4549680..0000000 --- a/gomod2nix.toml +++ /dev/null @@ -1,147 +0,0 @@ -schema = 3 - -[mod] - [mod."github.com/76creates/stickers"] - version = "v1.4.1" - hash = "sha256-nUvY5MyiIXk4PbkLyx14b2yryjCjUedyy2un/RYnksw=" - [mod."github.com/anmitsu/go-shlex"] - version = "v0.0.0-20200514113438-38f4b401e2be" - hash = "sha256-L3Ak4X2z7WXq7vMKuiHCOJ29nlpajUQ08Sfb9T0yP54=" - [mod."github.com/atotto/clipboard"] - version = "v0.1.4" - hash = "sha256-ZZ7U5X0gWOu8zcjZcWbcpzGOGdycwq0TjTFh/eZHjXk=" - [mod."github.com/aymanbagabas/go-osc52/v2"] - version = "v2.0.1" - hash = "sha256-6Bp0jBZ6npvsYcKZGHHIUSVSTAMEyieweAX2YAKDjjg=" - [mod."github.com/charmbracelet/bubbles"] - version = "v0.21.0" - hash = "sha256-cfjUHgy9eq5SretTHuYuRaeeT6QmJYQBB9dsI8QSnW0=" - [mod."github.com/charmbracelet/bubbletea"] - version = "v1.3.6" - hash = "sha256-79MpQk+92RDXnNc2hSct8EDf0AhV6MZpvFMmmrVC/bs=" - [mod."github.com/charmbracelet/colorprofile"] - version = "v0.2.3-0.20250311203215-f60798e515dc" - hash = "sha256-D9E/bMOyLXAUVOHA1/6o3i+vVmLfwIMOWib6sU7A6+Q=" - [mod."github.com/charmbracelet/keygen"] - version = "v0.5.3" - hash = "sha256-VUVU4BSanfpoKlESxjd8jm/pKBvekDnjP3mUqULjd+g=" - [mod."github.com/charmbracelet/lipgloss"] - version = "v1.1.0" - hash = "sha256-RHsRT2EZ1nDOElxAK+6/DC9XAaGVjDTgPvRh3pyCfY4=" - [mod."github.com/charmbracelet/log"] - version = "v0.4.2" - hash = "sha256-3w1PCM/c4JvVEh2d0sMfv4C77Xs1bPa1Ea84zdynC7I=" - [mod."github.com/charmbracelet/ssh"] - version = "v0.0.0-20250429213052-383d50896132" - hash = "sha256-vVJ6u0KVGW25uD39/IwgjCcWi/k/3DDIvpnRYb7j8zk=" - [mod."github.com/charmbracelet/wish"] - version = "v1.4.7" - hash = "sha256-wNg6APevKC8pi7aAt9fc81Wad1rMy/BgzVAsAu9Vo3U=" - [mod."github.com/charmbracelet/x/ansi"] - version = "v0.9.3" - hash = "sha256-Bzum17p7UQZeNxL155Pho/+GXj1DElB9Bp3O194CYf8=" - [mod."github.com/charmbracelet/x/cellbuf"] - version = "v0.0.13-0.20250311204145-2c3ea96c31dd" - hash = "sha256-XAhCOt8qJ2vR77lH1ez0IVU1/2CaLTq9jSmrHVg5HHU=" - [mod."github.com/charmbracelet/x/conpty"] - version = "v0.1.0" - hash = "sha256-VvqJl1WVm7ozO+Dmgl4e/iM9Z9FVrQLrLuA0Sr50KAM=" - [mod."github.com/charmbracelet/x/errors"] - version = "v0.0.0-20240508181413-e8d8b6e2de86" - hash = "sha256-GO8hf0lhVtl00C+xoTzvBtPU2cO0PymSLc2szBRUNtE=" - [mod."github.com/charmbracelet/x/input"] - version = "v0.3.4" - hash = "sha256-QdpH3Pfu04qMdQEnT80bJRJZIfMe4Cw8/HbM2OETZdM=" - [mod."github.com/charmbracelet/x/term"] - version = "v0.2.1" - hash = "sha256-VBkCZLI90PhMasftGw3403IqoV7d3E5WEGAIVrN5xQM=" - [mod."github.com/charmbracelet/x/termios"] - version = "v0.1.0" - hash = "sha256-cvyxWoAf3gDv17Bupd6X4fRkPPF87QU8F4vmsFw97QA=" - [mod."github.com/charmbracelet/x/windows"] - version = "v0.2.0" - hash = "sha256-pDAd1E5w66E/d3vuTyzgnW+W/KegZ2sxQQMfoEn7S1A=" - [mod."github.com/creack/pty"] - version = "v1.1.21" - hash = "sha256-pjGw6wQlrVhN65XaIxZueNJqnXThGu00u24rKOLzxS0=" - [mod."github.com/erikgeiser/coninput"] - version = "v0.0.0-20211004153227-1c3628e74d0f" - hash = "sha256-OWSqN1+IoL73rWXWdbbcahZu8n2al90Y3eT5Z0vgHvU=" - [mod."github.com/flywave/go-earcut"] - version = "v0.0.0-20210712015426-7084f78cceb3" - hash = "sha256-nhnbHWvvl5RD7DaQT+RNyl80AV/7vBGebtbPXbR0tso=" - [mod."github.com/go-logfmt/logfmt"] - version = "v0.6.0" - hash = "sha256-RtIG2qARd5sT10WQ7F3LR8YJhS8exs+KiuUiVf75bWg=" - [mod."github.com/gogo/protobuf"] - version = "v1.3.2" - hash = "sha256-pogILFrrk+cAtb0ulqn9+gRZJ7sGnnLLdtqITvxvG6c=" - [mod."github.com/google/go-querystring"] - version = "v1.1.0" - hash = "sha256-itsKgKghuX26czU79cK6C2n+lc27jm5Dw1XbIRgwZJY=" - [mod."github.com/joho/godotenv"] - version = "v1.5.1" - hash = "sha256-kA0osKfsc6Kp+nuGTRJyXZZlJt1D/kuEazKMWYCWcQ8=" - [mod."github.com/lucasb-eyer/go-colorful"] - version = "v1.2.0" - hash = "sha256-Gg9dDJFCTaHrKHRR1SrJgZ8fWieJkybljybkI9x0gyE=" - [mod."github.com/mattn/go-isatty"] - version = "v0.0.20" - hash = "sha256-qhw9hWtU5wnyFyuMbKx+7RB8ckQaFQ8D+8GKPkN3HHQ=" - [mod."github.com/mattn/go-localereader"] - version = "v0.0.1" - hash = "sha256-JlWckeGaWG+bXK8l8WEdZqmSiTwCA8b1qbmBKa/Fj3E=" - [mod."github.com/mattn/go-runewidth"] - version = "v0.0.16" - hash = "sha256-NC+ntvwIpqDNmXb7aixcg09il80ygq6JAnW0Gb5b/DQ=" - [mod."github.com/mattn/go-sqlite3"] - version = "v1.14.28" - hash = "sha256-mskU1xki6J1Fj6ItNgY/XNetB4Ta4jufEr4+JvTd7qs=" - [mod."github.com/muesli/ansi"] - version = "v0.0.0-20230316100256-276c6243b2f6" - hash = "sha256-qRKn0Bh2yvP0QxeEMeZe11Vz0BPFIkVcleKsPeybKMs=" - [mod."github.com/muesli/cancelreader"] - version = "v0.2.2" - hash = "sha256-uEPpzwRJBJsQWBw6M71FDfgJuR7n55d/7IV8MO+rpwQ=" - [mod."github.com/muesli/termenv"] - version = "v0.16.0" - hash = "sha256-hGo275DJlyLtcifSLpWnk8jardOksdeX9lH4lBeE3gI=" - [mod."github.com/paulmach/orb"] - version = "v0.11.1" - hash = "sha256-oh5J5jl15FZwNm0jiYvPxNprMbufuSOELtvYHk1LLNs=" - [mod."github.com/paulmach/protoscan"] - version = "v0.2.1" - hash = "sha256-K5BOkSti6rgMofgdH72FgvFX0++KZsYI+QqKHZg2S5c=" - [mod."github.com/rivo/uniseg"] - version = "v0.4.7" - hash = "sha256-rDcdNYH6ZD8KouyyiZCUEy8JrjOQoAkxHBhugrfHjFo=" - [mod."github.com/sahilm/fuzzy"] - version = "v0.1.1" - hash = "sha256-f2VsDI6G+V2w31tSDzbZPi9EI2E7jRV6Aq8yeOorSZY=" - [mod."github.com/tidwall/geoindex"] - version = "v1.7.0" - hash = "sha256-Miz4xfkxm6iPE1Odw1I301RBaq4nCOW/lkLGLPYKlPA=" - [mod."github.com/tidwall/rtree"] - version = "v1.10.0" - hash = "sha256-/XdIMDlNoxS8v15pjJ25cW2xMUC7zfnywz5lezhgkwE=" - [mod."github.com/xo/terminfo"] - version = "v0.0.0-20220910002029-abceb7e1c41e" - hash = "sha256-GyCDxxMQhXA3Pi/TsWXpA8cX5akEoZV7CFx4RO3rARU=" - [mod."go.mongodb.org/mongo-driver"] - version = "v1.11.4" - hash = "sha256-Db3G9xc1JKJbscSdXKmgH5TDZylRZU5WNshqkt16z4A=" - [mod."golang.org/x/crypto"] - version = "v0.37.0" - hash = "sha256-9NwDEcii1e2JYM/+3y1yNzWnt/ChMm27e9OtfuF39OM=" - [mod."golang.org/x/exp"] - version = "v0.0.0-20240719175910-8a7402abbf56" - hash = "sha256-mHEPy0vbd/pFwq5ZAEKaehCeYVQLEFDGnXAoVgkCLPo=" - [mod."golang.org/x/sync"] - version = "v0.15.0" - hash = "sha256-Jf4ehm8H8YAWY6mM151RI5CbG7JcOFtmN0AZx4bE3UE=" - [mod."golang.org/x/sys"] - version = "v0.33.0" - hash = "sha256-wlOzIOUgAiGAtdzhW/KPl/yUVSH/lvFZfs5XOuJ9LOQ=" - [mod."golang.org/x/text"] - version = "v0.26.0" - hash = "sha256-N+27nBCyGvje0yCTlUzZoVZ0LRxx4AJ+eBlrFQVRlFQ=" diff --git a/main.go b/main.go deleted file mode 100644 index 4db518e..0000000 --- a/main.go +++ /dev/null @@ -1,129 +0,0 @@ -package main - -import ( - "context" - "errors" - "flag" - "os" - "os/signal" - "path/filepath" - "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", err) - } - - /* TODO: convert to viper for flags & config */ - 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 in user config directory */ - /* TODO: change log directory? */ - confDir, err := os.UserConfigDir() - if err != nil { - panic(err) - } - - tripConfigDir := filepath.Join(confDir, "trip") - if err := os.MkdirAll(tripConfigDir, 0755); err != nil { - panic(err) - } - - f, err := os.OpenFile(filepath.Join(tripConfigDir, "trip.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); - 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 25b9aa1..0000000 --- a/rendermaps/cache.go +++ /dev/null @@ -1,58 +0,0 @@ -package rendermaps - -import ( - "os" - "path/filepath" -) - -var localCache string - -func createCache() error { - userCache, err := os.UserCacheDir() - if err != nil { - return err - } - - cache := filepath.Join(userCache, "trip") - if err := os.MkdirAll(cache, 0755); err != nil { - return err - } - - localCache = cache - return nil -} - -/* used to prevent creating files on import */ -func ensureCache() error { - if localCache != "" { - return nil - } - - return createCache() -} - -func cacheInsertKey(key string, value []byte) { - err := ensureCache() - if err != nil { - panic(err) - } - - cacheFile := filepath.Join(localCache, key) - if err := os.WriteFile(cacheFile, value, 0644); err != nil { - panic(err) - } -} - -func cacheGetKey(key string) ([]byte, error) { - err := ensureCache() - if err != nil { - panic(err) - } - - 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 index 41d5401..f1e67b1 100644 --- a/shell.nix +++ b/shell.nix @@ -1,25 +1,33 @@ { - pkgs ? ( - let - inherit (builtins) fetchTree fromJSON readFile; - inherit ((fromJSON (readFile ./flake.lock)).nodes) nixpkgs gomod2nix; - in - import (fetchTree nixpkgs.locked) { - overlays = [ - (import "${fetchTree gomod2nix.locked}/overlay.nix") - ]; - } - ), - mkGoEnv ? pkgs.mkGoEnv, - gomod2nix ? pkgs.gomod2nix, + pkgs ? import { }, }: let - goEnv = mkGoEnv { pwd = ./.; }; + overrides = (builtins.fromTOML (builtins.readFile ./rust-toolchain.toml)); in -pkgs.mkShell { - packages = [ - goEnv - gomod2nix - ]; -} +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/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) -} - From 4da413bed891e7d06a6eab5d60fd1da7be9eacd2 Mon Sep 17 00:00:00 2001 From: isobelmcrae Date: Sun, 21 Dec 2025 00:37:39 +1100 Subject: [PATCH 4/4] feat(maps): unfinished map port, completed cache & projection --- src/maps/cache.rs | 35 +++++++++++ src/maps/mod.rs | 3 + src/maps/proj.rs | 147 ++++++++++++++++++++++++++++++++++++++++++++++ src/maps/style.rs | 35 +++++++++++ 4 files changed, 220 insertions(+) create mode 100644 src/maps/cache.rs create mode 100644 src/maps/mod.rs create mode 100644 src/maps/proj.rs create mode 100644 src/maps/style.rs 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, +}