-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathinfo.lua
More file actions
201 lines (181 loc) · 7.19 KB
/
Copy pathinfo.lua
File metadata and controls
201 lines (181 loc) · 7.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
-- info (Lua port) — top-right always-on chrome showing centre /
-- cursor / zoom / pan speed + bearing / solar time / distance from
-- the user's IP-located home / reverse-geocoded place name.
--
-- The reverse-geocode lookup hits Nominatim's free `/reverse`
-- endpoint with a 5 s throttle (the upstream asks callers to stay
-- under 1 req/s; 5 s is comfortably under). Throttle is plugin-side
-- because the rest of the painted readout updates every frame.
--
-- Pan speed and bearing are derived from the haversine distance and
-- initial bearing between the centre at the start of the current
-- 1 s window and the centre now. `os.time()` gives integer-second
-- resolution which is plenty for a glance — the display is read at
-- human cadence, not frame cadence.
--
-- Solar time is `UTC + lon / 15h`, normalised to 0–24h. Pure mean
-- solar time — no time zones, no DST, just where the sun is in the
-- sky at this longitude.
local fmt = require "ttymap.fmt"
local geo = require "ttymap.geo"
local loc = require "ttymap.location"
local NOMINATIM_URL = "https://nominatim.openstreetmap.org/reverse"
local INTERVAL_SEC = 5
local state = {
place_name = nil,
job = nil,
last_query = nil, -- "lat,lon" string for the in-flight or last fetch
last_fetch_sec = 0,
-- Pan-speed / bearing bookkeeping. We anchor at one position per
-- integer second; when the second rolls over, we recompute both
-- off the same anchor pair (so the bearing reflects exactly the
-- direction the second's speed was earned in).
speed_anchor_lat = nil,
speed_anchor_lon = nil,
speed_anchor_sec = nil,
speed_mps = 0,
bearing = nil, -- compass label N/NE/.../NW, nil when idle
}
-- Kick the geoip resolution once at plugin load so `from here:` has
-- a value to display by the time the user notices the row. Empty cb
-- — we don't care about the result here; `loc.cached()` picks it up
-- on subsequent ticks.
loc.get(function() end)
local function reverse_url(lat, lon)
return string.format("%s?lat=%f&lon=%f&format=json&zoom=10",
NOMINATIM_URL, lat, lon)
end
local function format_place(payload)
if not payload then return nil end
local addr = payload.address
local city
if addr then
city = addr.city or addr.town or addr.village
end
local country = addr and addr.country
if city and country then
return city .. ", " .. country
elseif country then
return country
elseif city then
return city
elseif payload.display_name then
return payload.display_name
end
return nil
end
local function format_speed(mps)
if mps < 1 then return "idle" end
if mps < 1000 then return string.format("%d m/s", math.floor(mps + 0.5)) end
return string.format("%.1f km/s", mps / 1000)
end
-- Mean solar time at `lon` derived from the system UTC clock —
-- 15° east of Greenwich is +1h, antimeridian is ±12h, normalised to
-- the 0–24h range. No DST, no time zones — this is "where is the
-- sun" not "what time is it on someone's wristwatch".
local function solar_time(lon)
local t = os.date("!*t", os.time())
local utc_h = t.hour + t.min / 60 + t.sec / 3600
local h = (utc_h + lon / 15) % 24
if h < 0 then h = h + 24 end
local hh = math.floor(h)
local mm = math.floor((h - hh) * 60 + 0.5)
if mm == 60 then mm = 0; hh = (hh + 1) % 24 end
return string.format("%02d:%02d", hh, mm)
end
-- Update state.speed_mps + state.bearing once per integer second
-- based on the haversine distance and bearing between the second's
-- anchor and the current centre. Within a second the displayed values
-- stay put — fine for glanceable UI and avoids divide-by-zero on
-- sub-second deltas.
local function tick_speed(lat, lon)
local now = os.time()
if state.speed_anchor_sec == nil then
state.speed_anchor_lat = lat
state.speed_anchor_lon = lon
state.speed_anchor_sec = now
return
end
local dt = now - state.speed_anchor_sec
if dt <= 0 then return end
local d = geo.haversine_m(state.speed_anchor_lat, state.speed_anchor_lon, lat, lon)
state.speed_mps = d / dt
-- Bearing only meaningful when there's a real displacement.
if d >= 1 then
state.bearing = geo.bearing_label(
state.speed_anchor_lat, state.speed_anchor_lon, lat, lon)
else
state.bearing = nil
end
state.speed_anchor_lat = lat
state.speed_anchor_lon = lon
state.speed_anchor_sec = now
end
local function refresh(lat, lon)
-- Always-running plugin; only kick a new fetch when the throttle
-- window has elapsed and no other request is in flight.
if state.job then return end
local now = os.time()
if (now - state.last_fetch_sec) < INTERVAL_SEC then return end
state.last_fetch_sec = now
state.last_query = string.format("%.4f,%.4f", lat, lon)
state.job = ttymap.http:fetch(reverse_url(lat, lon))
end
ttymap.api.frame.on_tick(function(map)
-- Drain the in-flight reverse-geocode job, if any.
if state.job then
local body = state.job:try_take()
if body then
local payload = ttymap.json:parse(body)
state.place_name = format_place(payload)
state.job = nil
end
end
local lon, lat = map:center()
local zoom = map:zoom()
-- Kick a new fetch (subject to the per-plugin throttle).
refresh(lat, lon)
tick_speed(lat, lon)
-- Layout grouped by topic, blank rows left as visual separators:
-- rows 0-2 position (center / cursor / zoom)
-- row 4 motion (speed + bearing)
-- rows 6-7 environment (solar time / place name)
-- row 9 relation (distance from user's home)
map:text_anchored("top-right", 0,
string.format(" center: %.3f, %.3f ", lat, lon), "accent")
local clon, clat = map:cursor()
local cursor_line
if clon and clat then
cursor_line = string.format(" cursor: %.3f, %.3f ", clat, clon)
else
cursor_line = " cursor: unknown "
end
map:text_anchored("top-right", 1, cursor_line, "accent")
map:text_anchored("top-right", 2,
string.format(" zoom: %.1f ", zoom), "accent")
local speed_line
if state.bearing then
speed_line = string.format(" speed: %s %s ",
format_speed(state.speed_mps), state.bearing)
else
speed_line = string.format(" speed: %s ", format_speed(state.speed_mps))
end
map:text_anchored("top-right", 4, speed_line, "accent")
map:text_anchored("top-right", 6,
string.format(" solar: %s ", solar_time(lon)), "accent")
local place = state.place_name or "unknown"
map:text_anchored("top-right", 7,
" place: " .. place .. " ", "accent")
-- "from here" — distance from the user's IP-located home to the
-- current centre. `loc.cached()` returns nil until the initial
-- geoip lookup lands, in which case we show "unknown".
local hlat, hlon = loc.cached()
local from_here_line
if hlat and hlon then
local d = geo.haversine_m(hlat, hlon, lat, lon)
from_here_line = " from here: " .. fmt.distance(d) .. " "
else
from_here_line = " from here: unknown "
end
map:text_anchored("top-right", 9, from_here_line, "accent")
end)