Compare commits
2
Commits
67585cd99a
...
0c4ee97368
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c4ee97368 | ||
|
|
141b7a08ce |
No files matched your search
@@ -325,6 +325,25 @@ module Maptool
|
||||
json sessions: LIVETRACK_STORE.list
|
||||
end
|
||||
|
||||
# Garmin stops serving a session's live-track page some while after the
|
||||
# activity ends (its own session/token URL, not the API — see
|
||||
# LiveTrackClient) — the exact cutoff isn't documented, ~24h is what's
|
||||
# been observed. Ended sessions old enough to likely be past that are
|
||||
# verified once at startup (LiveTrackStore#stale_ended already skips
|
||||
# ones already flagged, so this doesn't re-hit Garmin for the same
|
||||
# dead session on every restart), so the panel can grey them out up
|
||||
# front instead of only failing after a user clicks one and waits on
|
||||
# a fetch.
|
||||
LIVETRACK_MAP_STALE_AFTER = 24 * 60 * 60 # seconds
|
||||
|
||||
Thread.new do
|
||||
LIVETRACK_STORE.stale_ended(LIVETRACK_MAP_STALE_AFTER).each do |session|
|
||||
LiveTrackClient.fetch(session["id"], session["token"])
|
||||
rescue LiveTrackClient::FetchError
|
||||
LIVETRACK_STORE.mark_unavailable(session["id"])
|
||||
end
|
||||
end
|
||||
|
||||
# Garmin's own API (livetrack.garmin.com/api/v2/...) 403s a plain server
|
||||
# request no matter what headers it gets — see LiveTrackClient — so this
|
||||
# fetches the session page itself and pulls the track out of that.
|
||||
|
||||
@@ -55,6 +55,31 @@ module Maptool
|
||||
end
|
||||
end
|
||||
|
||||
# Ended sessions (summary["active"] == false) that ended more than
|
||||
# `stale_after` seconds ago and aren't already flagged unavailable —
|
||||
# i.e. ones Garmin has very likely stopped serving the live-track page
|
||||
# for, but that haven't been confirmed yet (see app.rb's startup
|
||||
# check). Excludes already-flagged ones so that check doesn't re-hit
|
||||
# Garmin, on every restart, for a session it already told us is gone.
|
||||
def stale_ended(stale_after)
|
||||
cutoff = Time.now.utc - stale_after
|
||||
list.select { |s| stale_ended?(s, cutoff) }
|
||||
end
|
||||
|
||||
# Flags a session's live-track page as no longer served by Garmin, once
|
||||
# confirmed (see app.rb's startup check) — the summary itself (small,
|
||||
# and a record of the activity, see the class comment) is left alone.
|
||||
def mark_unavailable(session_id)
|
||||
@mutex.synchronize do
|
||||
sessions = read
|
||||
session = sessions.find { |s| s["id"] == session_id }
|
||||
return unless session
|
||||
|
||||
session["map_available"] = false
|
||||
write(sessions)
|
||||
end
|
||||
end
|
||||
|
||||
# Most recently received first.
|
||||
def list
|
||||
prune(read)
|
||||
@@ -77,6 +102,20 @@ module Maptool
|
||||
Time.now.utc # malformed/missing timestamp: don't let it block pruning
|
||||
end
|
||||
|
||||
def stale_ended?(session, cutoff)
|
||||
summary = session["summary"]
|
||||
return false unless summary && summary["active"] == false && session["map_available"] != false
|
||||
|
||||
ended = ended_at(summary)
|
||||
ended && ended < cutoff
|
||||
end
|
||||
|
||||
def ended_at(summary)
|
||||
Time.parse(summary["end"])
|
||||
rescue ArgumentError, TypeError
|
||||
nil # malformed/missing "end": can't tell how long it's been, so don't guess
|
||||
end
|
||||
|
||||
def write(sessions)
|
||||
File.write(@path, JSON.pretty_generate(sessions))
|
||||
end
|
||||
|
||||
@@ -781,6 +781,11 @@ html, body {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.livetrack-panel__row:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.livetrack-panel__label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
@@ -76,15 +76,18 @@ export class LiveTrackPanelControl {
|
||||
return;
|
||||
}
|
||||
|
||||
sessions.forEach(({ id, received_at, summary }) => {
|
||||
sessions.forEach(({ id, received_at, summary, map_available }) => {
|
||||
const li = document.createElement("li");
|
||||
li.className = "livetrack-panel__item";
|
||||
|
||||
const unavailable = map_available === false;
|
||||
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "livetrack-panel__row";
|
||||
button.classList.toggle("active", id === this._activeSessionId);
|
||||
button.title = id;
|
||||
button.disabled = unavailable;
|
||||
button.title = unavailable ? "Garmin no longer serves this session's map" : id;
|
||||
|
||||
const labelSpan = document.createElement("span");
|
||||
labelSpan.className = "livetrack-panel__label";
|
||||
@@ -100,7 +103,7 @@ export class LiveTrackPanelControl {
|
||||
} else {
|
||||
button.append(labelSpan, timeSpan);
|
||||
}
|
||||
button.addEventListener("click", () => this._toggleTrack(id, button));
|
||||
if (!unavailable) button.addEventListener("click", () => this._toggleTrack(id, button));
|
||||
li.appendChild(button);
|
||||
this._list.appendChild(li);
|
||||
});
|
||||
|
||||
+38
-2
@@ -18,7 +18,10 @@ export class LocateControl {
|
||||
this._gnssControl = gnssControl;
|
||||
this._active = false;
|
||||
this._marker = null;
|
||||
this._circleMarker = null;
|
||||
this._accuracy = null;
|
||||
this._watchId = null;
|
||||
this._onMapChange = () => this._updateCircleRadius();
|
||||
}
|
||||
|
||||
onAdd(map) {
|
||||
@@ -102,13 +105,15 @@ export class LocateControl {
|
||||
this._active = true;
|
||||
this._button.classList.add("maplibregl-ctrl-geolocate-active");
|
||||
this._watchId = navigator.geolocation.watchPosition(
|
||||
(position) => this._setPosition(position.coords.longitude, position.coords.latitude),
|
||||
(position) => this._setPosition(position.coords.longitude, position.coords.latitude, position.coords.accuracy),
|
||||
() => {
|
||||
window.alert("Could not determine your location.");
|
||||
this._stop();
|
||||
},
|
||||
{ enableHighAccuracy: true },
|
||||
);
|
||||
this._map.on("zoom", this._onMapChange);
|
||||
this._map.on("move", this._onMapChange);
|
||||
}
|
||||
|
||||
// Recenters on every fix, following the position the way GeolocateControl
|
||||
@@ -123,16 +128,40 @@ export class LocateControl {
|
||||
// .gnss-marker — reusing that green for a browser-estimated position too
|
||||
// looked like this control was somehow still using GNSS data even when
|
||||
// it very much wasn't; distinct colors for distinct sources.
|
||||
_setPosition(lon, lat) {
|
||||
_setPosition(lon, lat, accuracy) {
|
||||
this._accuracy = accuracy;
|
||||
if (!this._marker) {
|
||||
const el = document.createElement("div");
|
||||
el.className = "maplibregl-user-location-dot";
|
||||
this._marker = new Marker({ element: el }).setLngLat([lon, lat]).addTo(this._map);
|
||||
const circleEl = document.createElement("div");
|
||||
circleEl.className = "maplibregl-user-location-accuracy-circle";
|
||||
this._circleMarker = new Marker({ element: circleEl, pitchAlignment: "map" }).setLngLat([lon, lat]).addTo(this._map);
|
||||
this._map.flyTo({ center: [lon, lat], zoom: Math.max(this._map.getZoom(), 15) });
|
||||
} else {
|
||||
this._marker.setLngLat([lon, lat]);
|
||||
this._circleMarker.setLngLat([lon, lat]);
|
||||
this._map.easeTo({ center: [lon, lat] });
|
||||
}
|
||||
this._updateCircleRadius();
|
||||
}
|
||||
|
||||
// Mirrors MapLibre's own GeolocateControl._updateCircleRadiusIfNeeded:
|
||||
// measures real-world meters per pixel at the marker's position (rather
|
||||
// than assuming a flat conversion, which breaks away from the equator)
|
||||
// by projecting a 100px offset back to lng/lat and measuring that
|
||||
// distance, then sizes the circle element to the accuracy radius in
|
||||
// that many pixels.
|
||||
_updateCircleRadius() {
|
||||
if (!this._circleMarker || !this._accuracy) return;
|
||||
|
||||
const lngLat = this._marker.getLngLat();
|
||||
const point = this._map.project(lngLat);
|
||||
const offsetLngLat = this._map.unproject([point.x + 100, point.y]);
|
||||
const metersPerPixel = lngLat.distanceTo(offsetLngLat) / 100;
|
||||
const diameter = (2 * this._accuracy) / metersPerPixel;
|
||||
this._circleMarker.getElement().style.width = `${diameter.toFixed(2)}px`;
|
||||
this._circleMarker.getElement().style.height = `${diameter.toFixed(2)}px`;
|
||||
}
|
||||
|
||||
_stop() {
|
||||
@@ -142,9 +171,16 @@ export class LocateControl {
|
||||
navigator.geolocation.clearWatch(this._watchId);
|
||||
this._watchId = null;
|
||||
}
|
||||
this._map.off("zoom", this._onMapChange);
|
||||
this._map.off("move", this._onMapChange);
|
||||
if (this._marker) {
|
||||
this._marker.remove();
|
||||
this._marker = null;
|
||||
}
|
||||
if (this._circleMarker) {
|
||||
this._circleMarker.remove();
|
||||
this._circleMarker = null;
|
||||
}
|
||||
this._accuracy = null;
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,35 @@ class LiveTrackStoreTest < Minitest::Test
|
||||
assert_includes ids, "old_with_summary"
|
||||
end
|
||||
|
||||
def test_stale_ended_finds_old_ended_sessions_but_not_active_recent_or_already_flagged_ones
|
||||
@store.add("old_ended", "tok1")
|
||||
@store.set_summary("old_ended", { "active" => false, "end" => (Time.now.utc - 25 * 60 * 60).iso8601 })
|
||||
|
||||
@store.add("recent_ended", "tok2")
|
||||
@store.set_summary("recent_ended", { "active" => false, "end" => Time.now.utc.iso8601 })
|
||||
|
||||
@store.add("still_active", "tok3")
|
||||
@store.set_summary("still_active", { "active" => true })
|
||||
|
||||
@store.add("already_flagged", "tok4")
|
||||
@store.set_summary("already_flagged", { "active" => false, "end" => (Time.now.utc - 25 * 60 * 60).iso8601 })
|
||||
@store.mark_unavailable("already_flagged")
|
||||
|
||||
ids = @store.stale_ended(24 * 60 * 60).map { |s| s["id"] }
|
||||
assert_equal ["old_ended"], ids
|
||||
end
|
||||
|
||||
def test_mark_unavailable_flags_the_session_without_touching_its_summary
|
||||
@store.add("abc123", "tok-abc")
|
||||
@store.set_summary("abc123", { "session_name" => "Test Ride" })
|
||||
|
||||
@store.mark_unavailable("abc123")
|
||||
|
||||
session = @store.find("abc123")
|
||||
assert_equal false, session["map_available"]
|
||||
assert_equal({ "session_name" => "Test Ride" }, session["summary"])
|
||||
end
|
||||
|
||||
def test_data_survives_reload_from_disk
|
||||
@store.add("abc123", "tok-abc")
|
||||
path = @store.instance_variable_get(:@path)
|
||||
|
||||
Reference in new issue
Block a user