- Replace placeholder SVG icons with custom PNG icons (80x80 padded) - AffineTransform for pixel-perfect icon centering in menu ring - Colored circles behind icons with icon drawn at 60% size - GPS: always wait full timeout (30s), keep best quality fix - History: show address, PLZ+city, coordinates on separate lines - Geocoding: fall back to "name" field for street-level results, include PLZ+city in cached address data - Address distance threshold raised to 70m Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
238 lines
8.5 KiB
MonkeyC
238 lines
8.5 KiB
MonkeyC
import Toybox.Graphics;
|
|
import Toybox.Lang;
|
|
import Toybox.Time;
|
|
import Toybox.Time.Gregorian;
|
|
import Toybox.WatchUi;
|
|
|
|
// Scrollable history of recorded events. 3-section layout:
|
|
// top 15%: previous entry icon + up arrow
|
|
// mid 70%: current entry details (type, datetime, address/coords)
|
|
// bot 15%: next entry icon + down arrow
|
|
class HistoryView extends WatchUi.View {
|
|
|
|
private var _events as Array<Event>;
|
|
private var _index as Number = 0;
|
|
private var _bitmaps as Dictionary = {};
|
|
private var _addressCache as Dictionary = {}; // index → String or Null
|
|
private var _addressLoading as Boolean = false;
|
|
private var _geocoder as GeocodingService or Null = null;
|
|
|
|
function initialize() {
|
|
View.initialize();
|
|
_events = EventStore.getAll();
|
|
// Start at newest entry.
|
|
if (_events.size() > 0) {
|
|
_index = _events.size() - 1;
|
|
}
|
|
}
|
|
|
|
function onLayout(dc as Dc) as Void {
|
|
var items = Config.menuItems();
|
|
for (var i = 0; i < items.size(); i++) {
|
|
var iconId = items[i][:icon];
|
|
_bitmaps[items[i][:key]] = WatchUi.loadResource(iconId) as BitmapResource;
|
|
}
|
|
}
|
|
|
|
function eventCount() as Number {
|
|
return _events.size();
|
|
}
|
|
|
|
function showNext() as Void {
|
|
if (_events.size() == 0) { return; }
|
|
_index = (_index + 1) % _events.size();
|
|
_addressLoading = false;
|
|
_requestAddress();
|
|
WatchUi.requestUpdate();
|
|
}
|
|
|
|
function showPrev() as Void {
|
|
if (_events.size() == 0) { return; }
|
|
_index = (_index - 1 + _events.size()) % _events.size();
|
|
_addressLoading = false;
|
|
_requestAddress();
|
|
WatchUi.requestUpdate();
|
|
}
|
|
|
|
function onShow() as Void {
|
|
_requestAddress();
|
|
}
|
|
|
|
private function _requestAddress() as Void {
|
|
if (_events.size() == 0) { return; }
|
|
if (_addressCache.hasKey(_index)) { return; }
|
|
var evt = _events[_index];
|
|
// Use cached address from event if available.
|
|
if (evt.address != null) {
|
|
_addressCache[_index] = { "addr" => evt.address, "zip" => evt.zip };
|
|
return;
|
|
}
|
|
if (evt.lat == null || evt.lon == null) { return; }
|
|
_addressLoading = true;
|
|
_geocoder = new GeocodingService(
|
|
evt.lat as Float, evt.lon as Float,
|
|
method(:_onAddress)
|
|
);
|
|
_geocoder.start();
|
|
}
|
|
|
|
function _onAddress(result as Dictionary or Null) as Void {
|
|
_addressCache[_index] = result;
|
|
_addressLoading = false;
|
|
// Persist in the event so the glance can show it.
|
|
if (result != null && _index < _events.size()) {
|
|
var evt = _events[_index];
|
|
evt.address = result["addr"] as String or Null;
|
|
evt.zip = result["zip"] as String or Null;
|
|
EventStore.updateAt(_index, evt);
|
|
}
|
|
WatchUi.requestUpdate();
|
|
}
|
|
|
|
function onUpdate(dc as Dc) as Void {
|
|
dc.setColor(Config.COLOR_FG, Config.COLOR_BG);
|
|
dc.clear();
|
|
|
|
if (_events.size() == 0) {
|
|
var cx = LayoutMetrics.centerX(dc);
|
|
var cy = LayoutMetrics.centerY(dc);
|
|
TextUtils.drawResourceCentered(dc, Rez.Strings.history_empty, cx, cy, Graphics.FONT_MEDIUM);
|
|
return;
|
|
}
|
|
|
|
var cx = LayoutMetrics.centerX(dc);
|
|
var topH = LayoutMetrics.topSectionHeight(dc);
|
|
var botY = LayoutMetrics.bottomSectionY(dc);
|
|
var screenH = dc.getHeight();
|
|
|
|
_drawTopSection(dc, cx, topH);
|
|
_drawMiddleSection(dc, cx, topH, botY);
|
|
_drawBottomSection(dc, cx, botY, screenH);
|
|
}
|
|
|
|
private function _drawTopSection(dc as Dc, cx as Number, topH as Number) as Void {
|
|
if (_events.size() <= 1) { return; }
|
|
var midY = topH / 2;
|
|
var arrowSize = (topH * 0.2).toNumber();
|
|
dc.setColor(Config.COLOR_FG, Config.COLOR_BG);
|
|
dc.setPenWidth(2);
|
|
dc.drawLine(cx, midY - arrowSize, cx, midY + arrowSize);
|
|
dc.drawLine(cx - arrowSize, midY, cx, midY - arrowSize);
|
|
dc.drawLine(cx + arrowSize, midY, cx, midY - arrowSize);
|
|
}
|
|
|
|
private function _drawBottomSection(dc as Dc, cx as Number, botY as Number, screenH as Number) as Void {
|
|
if (_events.size() <= 1) { return; }
|
|
var sectionH = screenH - botY;
|
|
var midY = botY + sectionH / 2;
|
|
var arrowSize = (sectionH * 0.2).toNumber();
|
|
dc.setColor(Config.COLOR_FG, Config.COLOR_BG);
|
|
dc.setPenWidth(2);
|
|
dc.drawLine(cx, midY - arrowSize, cx, midY + arrowSize);
|
|
dc.drawLine(cx - arrowSize, midY, cx, midY + arrowSize);
|
|
dc.drawLine(cx + arrowSize, midY, cx, midY + arrowSize);
|
|
}
|
|
|
|
private function _drawMiddleSection(dc as Dc, cx as Number, topH as Number, botY as Number) as Void {
|
|
var evt = _events[_index];
|
|
var fontDetail = Graphics.FONT_XTINY;
|
|
var lineH = dc.getFontHeight(fontDetail);
|
|
var sectionMid = topH + (botY - topH) / 2;
|
|
|
|
// Collect lines to draw.
|
|
var lines = [] as Array<Array>; // [text, color]
|
|
|
|
// 1: Event type (colored).
|
|
lines.add([_eventLabel(evt.type), _eventColor(evt.type)]);
|
|
|
|
// 2: Date + time.
|
|
lines.add([_formatTimestamp(evt.timestamp), Config.COLOR_FG]);
|
|
|
|
// 3: Address or status.
|
|
if (evt.lat == null || evt.lon == null) {
|
|
lines.add([WatchUi.loadResource(Rez.Strings.history_no_gps) as String, Config.COLOR_FG]);
|
|
} else if (_addressCache.hasKey(_index)) {
|
|
var cached = _addressCache[_index] as Dictionary or Null;
|
|
if (cached != null) {
|
|
lines.add([cached["addr"] as String, Config.COLOR_FG]);
|
|
var zip = cached["zip"] as String or Null;
|
|
if (zip != null) {
|
|
lines.add([zip, 0x888888]);
|
|
}
|
|
}
|
|
} else if (_addressLoading) {
|
|
lines.add([WatchUi.loadResource(Rez.Strings.history_loading_address) as String, 0x888888]);
|
|
}
|
|
|
|
// 4: Coordinates (always, if available).
|
|
if (evt.lat != null && evt.lon != null) {
|
|
lines.add([_formatCoords(evt.lat as Float, evt.lon as Float), 0x888888]);
|
|
}
|
|
|
|
// 5: Counter.
|
|
lines.add([(_index + 1) + "/" + _events.size(), 0x666666]);
|
|
|
|
// Draw centered vertically.
|
|
var totalH = lines.size() * lineH;
|
|
var startY = sectionMid - totalH / 2 + lineH / 2;
|
|
for (var i = 0; i < lines.size(); i++) {
|
|
dc.setColor(lines[i][1] as Number, Config.COLOR_BG);
|
|
dc.drawText(cx, startY + i * lineH, fontDetail, lines[i][0] as String,
|
|
Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
|
|
}
|
|
}
|
|
|
|
private function _formatCoords(lat as Float, lon as Float) as String {
|
|
return lat.format("%.4f") + ", " + lon.format("%.4f");
|
|
}
|
|
|
|
private function _formatTimestamp(ts as Number) as String {
|
|
var moment = new Time.Moment(ts);
|
|
var info = Gregorian.info(moment, Time.FORMAT_SHORT);
|
|
return Lang.format("$1$.$2$.$3$ $4$:$5$", [
|
|
info.day.format("%02d"),
|
|
info.month.format("%02d"),
|
|
info.year,
|
|
info.hour.format("%02d"),
|
|
info.min.format("%02d")
|
|
]);
|
|
}
|
|
|
|
private function _eventLabel(key as String) as String {
|
|
var items = Config.menuItems();
|
|
for (var i = 0; i < items.size(); i++) {
|
|
if ((items[i][:key] as String).equals(key)) {
|
|
var lines = items[i][:lines] as Array<String>;
|
|
var result = "";
|
|
for (var j = 0; j < lines.size(); j++) {
|
|
if (j > 0) { result += ""; }
|
|
result += lines[j];
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
return key;
|
|
}
|
|
|
|
private function _eventColor(key as String) as Number {
|
|
var items = Config.menuItems();
|
|
for (var i = 0; i < items.size(); i++) {
|
|
if ((items[i][:key] as String).equals(key)) {
|
|
return items[i][:color] as Number;
|
|
}
|
|
}
|
|
return Config.COLOR_FG;
|
|
}
|
|
|
|
private function _drawEventIcon(dc as Dc, key as String, cx as Number, cy as Number, size as Number) as Void {
|
|
var bmp = _bitmaps[key] as BitmapResource or Null;
|
|
if (bmp == null) { return; }
|
|
var bmpW = bmp.getWidth();
|
|
var scale = size.toFloat() / bmpW.toFloat();
|
|
dc.drawBitmap2(
|
|
cx - size / 2, cy - size / 2,
|
|
bmp,
|
|
{ :scaleX => scale, :scaleY => scale }
|
|
);
|
|
}
|
|
}
|