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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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);
+ }
+ }
+}