diff --git a/resources/drawables/drawables.xml b/resources/drawables/drawables.xml index 822fe90..67da7a7 100644 --- a/resources/drawables/drawables.xml +++ b/resources/drawables/drawables.xml @@ -1,3 +1,13 @@ - + + + + + + + + + + + diff --git a/resources/drawables/icon_arrest.svg b/resources/drawables/icon_arrest.svg new file mode 100644 index 0000000..0c1a65c --- /dev/null +++ b/resources/drawables/icon_arrest.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/drawables/icon_arrival.svg b/resources/drawables/icon_arrival.svg new file mode 100644 index 0000000..eec8e37 --- /dev/null +++ b/resources/drawables/icon_arrival.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/drawables/icon_delete.svg b/resources/drawables/icon_delete.svg new file mode 100644 index 0000000..6f98e07 --- /dev/null +++ b/resources/drawables/icon_delete.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/drawables/icon_end.svg b/resources/drawables/icon_end.svg new file mode 100644 index 0000000..f9987a9 --- /dev/null +++ b/resources/drawables/icon_end.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/drawables/icon_event.svg b/resources/drawables/icon_event.svg new file mode 100644 index 0000000..dc842c3 --- /dev/null +++ b/resources/drawables/icon_event.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/drawables/icon_evidence.svg b/resources/drawables/icon_evidence.svg new file mode 100644 index 0000000..378ac70 --- /dev/null +++ b/resources/drawables/icon_evidence.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/resources/drawables/icon_force.svg b/resources/drawables/icon_force.svg new file mode 100644 index 0000000..ff0ad00 --- /dev/null +++ b/resources/drawables/icon_force.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/resources/drawables/icon_history.svg b/resources/drawables/icon_history.svg new file mode 100644 index 0000000..e93fb6e --- /dev/null +++ b/resources/drawables/icon_history.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/drawables/icon_sighting.svg b/resources/drawables/icon_sighting.svg new file mode 100644 index 0000000..4a31eac --- /dev/null +++ b/resources/drawables/icon_sighting.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/drawables/icon_start.svg b/resources/drawables/icon_start.svg new file mode 100644 index 0000000..009b1c4 --- /dev/null +++ b/resources/drawables/icon_start.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/strings/strings.xml b/resources/strings/strings.xml index 2384dd1..de7f99f 100644 --- a/resources/strings/strings.xml +++ b/resources/strings/strings.xml @@ -1,3 +1,31 @@ Einsatzprotokoll + + + Verlauf + Ereignis + Einsatzbeginn + Einsatzende + Eintreffen + Festnahme + Zwanganwendung + Beweismittel + Sichtung + Letzten löschen + + + Standort wird bestimmt + Standort konnte nicht bestimmt werden + Fehler beim Speichern + + + Keine Einträge + Adresse wird geladen… + Verbinde Handy für Adresse + + + Halten zum Löschen + + + Keine Einträge diff --git a/source/Config.mc b/source/Config.mc new file mode 100644 index 0000000..73e2f9a --- /dev/null +++ b/source/Config.mc @@ -0,0 +1,62 @@ +import Toybox.Graphics; +import Toybox.Lang; + +// Central configuration. All tunable values live here so they can be +// changed without touching feature code. +module Config { + + // --- Retention ------------------------------------------------------ + const RETENTION_SEC = 7 * 24 * 60 * 60; + + // --- GPS ------------------------------------------------------------ + const GPS_TIMEOUT_MS = 10000; + const GPS_TARGET_ACCURACY_M = 5; + + // --- Animation / UI timings ---------------------------------------- + const SUCCESS_DISPLAY_MS = 2500; + const ERROR_DISPLAY_MS = 5000; + const DELETE_HOLD_MS = 2500; + + // --- Address resolution -------------------------------------------- + const ADDRESS_MAX_DISTANCE_M = 20; + const PHOTON_URL = "https://photon.komoot.io/reverse"; + + // --- Colors (AMOLED: black background, white fg) ------------------- + const COLOR_BG = 0x000000; + const COLOR_FG = 0xFFFFFF; + const COLOR_SUCCESS = 0x00AA00; + const COLOR_ERROR = 0xFF2222; + const COLOR_DELETE = 0xFF2222; + const COLOR_ACCENT = 0xFFFFFF; + + // --- Event type keys ------------------------------------------------ + const EVENT_GENERAL = "general"; + const EVENT_START = "start"; + const EVENT_END = "end"; + const EVENT_ARRIVAL = "arrival"; + const EVENT_ARREST = "arrest"; + const EVENT_FORCE = "force"; + const EVENT_EVIDENCE = "evidence"; + const EVENT_SIGHTING = "sighting"; + + // --- Special menu actions ------------------------------------------ + const ACTION_HISTORY = "history"; + const ACTION_DELETE = "delete"; + + // Menu item metadata. Order matches display order (index 0 is first + // selected). Each entry: key, drawable id, string id, highlight color. + function menuItems() as Array { + return [ + { :key => ACTION_HISTORY, :icon => Rez.Drawables.IconHistory, :label => Rez.Strings.menu_history, :color => 0xFFFFFF }, + { :key => EVENT_GENERAL, :icon => Rez.Drawables.IconEvent, :label => Rez.Strings.menu_general, :color => 0xFFAA00 }, + { :key => EVENT_START, :icon => Rez.Drawables.IconStart, :label => Rez.Strings.menu_start, :color => 0x00FF00 }, + { :key => EVENT_END, :icon => Rez.Drawables.IconEnd, :label => Rez.Strings.menu_end, :color => 0x00AAFF }, + { :key => EVENT_ARRIVAL, :icon => Rez.Drawables.IconArrival, :label => Rez.Strings.menu_arrival, :color => 0xFFFF00 }, + { :key => EVENT_ARREST, :icon => Rez.Drawables.IconArrest, :label => Rez.Strings.menu_arrest, :color => 0xFF0088 }, + { :key => EVENT_FORCE, :icon => Rez.Drawables.IconForce, :label => Rez.Strings.menu_force, :color => 0xAA00FF }, + { :key => EVENT_EVIDENCE, :icon => Rez.Drawables.IconEvidence, :label => Rez.Strings.menu_evidence, :color => 0x00FFFF }, + { :key => EVENT_SIGHTING, :icon => Rez.Drawables.IconSighting, :label => Rez.Strings.menu_sighting, :color => 0xFF8800 }, + { :key => ACTION_DELETE, :icon => Rez.Drawables.IconDelete, :label => Rez.Strings.menu_delete, :color => 0xFF2222 } + ]; + } +} diff --git a/source/EinsatzprotokollApp.mc b/source/EinsatzprotokollApp.mc index 3f94614..39fc72a 100644 --- a/source/EinsatzprotokollApp.mc +++ b/source/EinsatzprotokollApp.mc @@ -8,7 +8,10 @@ class EinsatzprotokollApp extends Application.AppBase { AppBase.initialize(); } - function onStart(state as Dictionary?) as Void {} + function onStart(state as Dictionary?) as Void { + EventStore.pruneOld(); + Logger.pruneOld(); + } function onStop(state as Dictionary?) as Void {} diff --git a/source/Event.mc b/source/Event.mc new file mode 100644 index 0000000..59c6bce --- /dev/null +++ b/source/Event.mc @@ -0,0 +1,41 @@ +import Toybox.Lang; + +// Value object for a protocol entry. Serialized as a plain Dictionary +// so Application.Storage can persist it. +class Event { + public var type as String; + public var timestamp as Number; // unix seconds + public var lat as Float or Null; + public var lon as Float or Null; + public var accuracy as Float or Null; + + function initialize(type as String, timestamp as Number, + lat as Float or Null, lon as Float or Null, + accuracy as Float or Null) { + self.type = type; + self.timestamp = timestamp; + self.lat = lat; + self.lon = lon; + self.accuracy = accuracy; + } + + function toDict() as Dictionary { + return { + "t" => type, + "ts" => timestamp, + "lat" => lat, + "lon" => lon, + "acc" => accuracy + }; + } + + static function fromDict(d as Dictionary) as Event { + return new Event( + d["t"] as String, + d["ts"] as Number, + d["lat"] as Float or Null, + d["lon"] as Float or Null, + d["acc"] as Float or Null + ); + } +} diff --git a/source/EventStore.mc b/source/EventStore.mc new file mode 100644 index 0000000..826349c --- /dev/null +++ b/source/EventStore.mc @@ -0,0 +1,73 @@ +import Toybox.Application; +import Toybox.Lang; +import Toybox.Time; + +// Persists events in Application.Storage as an array of dictionaries, +// ordered oldest → newest. Prunes entries older than Config.RETENTION_SEC +// whenever loaded. +module EventStore { + + const KEY = "events"; + + function getAll() as Array { + var raw = Application.Storage.getValue(KEY); + if (raw == null || !(raw instanceof Array)) { + return []; + } + var out = []; + for (var i = 0; i < raw.size(); i++) { + out.add(Event.fromDict(raw[i] as Dictionary)); + } + return out; + } + + function add(event as Event) as Void { + var raw = Application.Storage.getValue(KEY); + var arr = (raw instanceof Array) ? raw : []; + arr.add(event.toDict()); + Application.Storage.setValue(KEY, arr); + } + + function deleteLast() as Boolean { + var raw = Application.Storage.getValue(KEY); + if (!(raw instanceof Array) || raw.size() == 0) { + return false; + } + raw = raw.slice(0, raw.size() - 1); + Application.Storage.setValue(KEY, raw); + return true; + } + + function count() as Number { + var raw = Application.Storage.getValue(KEY); + return (raw instanceof Array) ? raw.size() : 0; + } + + function latest() as Event or Null { + var raw = Application.Storage.getValue(KEY); + if (!(raw instanceof Array) || raw.size() == 0) { + return null; + } + return Event.fromDict(raw[raw.size() - 1] as Dictionary); + } + + // Drops events with timestamp < (now - retention). + function pruneOld() as Void { + var raw = Application.Storage.getValue(KEY); + if (!(raw instanceof Array) || raw.size() == 0) { + return; + } + var cutoff = Time.now().value() - Config.RETENTION_SEC; + var kept = []; + for (var i = 0; i < raw.size(); i++) { + var d = raw[i] as Dictionary; + var ts = d["ts"]; + if (ts != null && (ts as Number) >= cutoff) { + kept.add(d); + } + } + if (kept.size() != raw.size()) { + Application.Storage.setValue(KEY, kept); + } + } +} diff --git a/source/Haversine.mc b/source/Haversine.mc new file mode 100644 index 0000000..633b1d7 --- /dev/null +++ b/source/Haversine.mc @@ -0,0 +1,25 @@ +import Toybox.Lang; +import Toybox.Math; + +// Great-circle distance between two WGS84 coordinates, in meters. +module Haversine { + + const EARTH_RADIUS_M = 6371000.0; + + function distance(lat1 as Float, lon1 as Float, lat2 as Float, lon2 as Float) as Float { + var dLat = _toRad(lat2 - lat1); + var dLon = _toRad(lon2 - lon1); + var rLat1 = _toRad(lat1); + var rLat2 = _toRad(lat2); + + var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(rLat1) * Math.cos(rLat2) + * Math.sin(dLon / 2) * Math.sin(dLon / 2); + var c = 2 * Math.asin(Math.sqrt(a)); + return (EARTH_RADIUS_M * c).toFloat(); + } + + function _toRad(deg as Float) as Float { + return (deg * Math.PI / 180.0).toFloat(); + } +} diff --git a/source/LayoutMetrics.mc b/source/LayoutMetrics.mc new file mode 100644 index 0000000..99b46d8 --- /dev/null +++ b/source/LayoutMetrics.mc @@ -0,0 +1,52 @@ +import Toybox.Graphics; +import Toybox.Lang; + +// Resolution-independent layout helpers. All sizes are computed as +// fractions of the drawable context so the app adapts to any round +// Garmin display. +module LayoutMetrics { + + // --- Menu ring geometry -------------------------------------------- + function centerX(dc as Dc) as Number { return dc.getWidth() / 2; } + function centerY(dc as Dc) as Number { return dc.getHeight() / 2; } + + // Radius on which the 10 icons sit. + function ringRadius(dc as Dc) as Number { + var min = (dc.getWidth() < dc.getHeight()) ? dc.getWidth() : dc.getHeight(); + return (min * 0.38).toNumber(); + } + + function iconSize(dc as Dc) as Number { + var min = (dc.getWidth() < dc.getHeight()) ? dc.getWidth() : dc.getHeight(); + return (min * 0.14).toNumber(); + } + + function selectedIconSize(dc as Dc) as Number { + return (iconSize(dc) * 1.3).toNumber(); + } + + // --- History view sections ----------------------------------------- + function topSectionHeight(dc as Dc) as Number { + return (dc.getHeight() * 0.15).toNumber(); + } + + function midSectionHeight(dc as Dc) as Number { + return (dc.getHeight() * 0.70).toNumber(); + } + + function bottomSectionY(dc as Dc) as Number { + return (dc.getHeight() * 0.85).toNumber(); + } + + // --- Loading / delete arc radius ------------------------------------ + function edgeArcRadius(dc as Dc) as Number { + var min = (dc.getWidth() < dc.getHeight()) ? dc.getWidth() : dc.getHeight(); + return (min * 0.48).toNumber(); + } + + function edgeArcPenWidth(dc as Dc) as Number { + var min = (dc.getWidth() < dc.getHeight()) ? dc.getWidth() : dc.getHeight(); + var w = (min * 0.025).toNumber(); + return (w < 3) ? 3 : w; + } +} diff --git a/source/Logger.mc b/source/Logger.mc new file mode 100644 index 0000000..605fcb7 --- /dev/null +++ b/source/Logger.mc @@ -0,0 +1,49 @@ +import Toybox.Application; +import Toybox.Lang; +import Toybox.Time; + +// Lightweight crash / diagnostic logger. Entries live in +// Application.Storage and are pruned with the same retention window +// as events so the watch storage never fills up. +module Logger { + + const KEY = "logs"; + const MAX_ENTRIES = 50; + + function log(msg as String) as Void { + var raw = Application.Storage.getValue(KEY); + var arr = (raw instanceof Array) ? raw : []; + arr.add({ + "ts" => Time.now().value(), + "msg" => msg + }); + if (arr.size() > MAX_ENTRIES) { + arr = arr.slice(arr.size() - MAX_ENTRIES, arr.size()); + } + Application.Storage.setValue(KEY, arr); + } + + function getAll() as Array { + var raw = Application.Storage.getValue(KEY); + return (raw instanceof Array) ? raw : []; + } + + function pruneOld() as Void { + var raw = Application.Storage.getValue(KEY); + if (!(raw instanceof Array) || raw.size() == 0) { + return; + } + var cutoff = Time.now().value() - Config.RETENTION_SEC; + var kept = []; + for (var i = 0; i < raw.size(); i++) { + var d = raw[i] as Dictionary; + var ts = d["ts"]; + if (ts != null && (ts as Number) >= cutoff) { + kept.add(d); + } + } + if (kept.size() != raw.size()) { + Application.Storage.setValue(KEY, kept); + } + } +}