diff --git a/resources/drawables/drawables.xml b/resources/drawables/drawables.xml index d0febbd..4923670 100644 --- a/resources/drawables/drawables.xml +++ b/resources/drawables/drawables.xml @@ -1,13 +1,13 @@ - - - - - - - - - - - + + + + + + + + + + + diff --git a/resources/drawables/icon_arrest.png b/resources/drawables/icon_arrest.png new file mode 100644 index 0000000..1a6f58c Binary files /dev/null and b/resources/drawables/icon_arrest.png differ diff --git a/resources/drawables/icon_arrival.png b/resources/drawables/icon_arrival.png new file mode 100644 index 0000000..72bcea2 Binary files /dev/null and b/resources/drawables/icon_arrival.png differ diff --git a/resources/drawables/icon_delete.png b/resources/drawables/icon_delete.png new file mode 100644 index 0000000..639faf0 Binary files /dev/null and b/resources/drawables/icon_delete.png differ diff --git a/resources/drawables/icon_end.png b/resources/drawables/icon_end.png new file mode 100644 index 0000000..643147d Binary files /dev/null and b/resources/drawables/icon_end.png differ diff --git a/resources/drawables/icon_event.png b/resources/drawables/icon_event.png new file mode 100644 index 0000000..b4788bf Binary files /dev/null and b/resources/drawables/icon_event.png differ diff --git a/resources/drawables/icon_evidence.png b/resources/drawables/icon_evidence.png new file mode 100644 index 0000000..f9e13a9 Binary files /dev/null and b/resources/drawables/icon_evidence.png differ diff --git a/resources/drawables/icon_force.png b/resources/drawables/icon_force.png new file mode 100644 index 0000000..beefd85 Binary files /dev/null and b/resources/drawables/icon_force.png differ diff --git a/resources/drawables/icon_history.png b/resources/drawables/icon_history.png new file mode 100644 index 0000000..1302a8b Binary files /dev/null and b/resources/drawables/icon_history.png differ diff --git a/resources/drawables/icon_sighting.png b/resources/drawables/icon_sighting.png new file mode 100644 index 0000000..b51c0de Binary files /dev/null and b/resources/drawables/icon_sighting.png differ diff --git a/resources/drawables/icon_start.png b/resources/drawables/icon_start.png new file mode 100644 index 0000000..cce6665 Binary files /dev/null and b/resources/drawables/icon_start.png differ diff --git a/resources/drawables/launcher_icon.png b/resources/drawables/launcher_icon.png new file mode 100644 index 0000000..6f8b276 Binary files /dev/null and b/resources/drawables/launcher_icon.png differ diff --git a/source/Config.mc b/source/Config.mc index 0eea8a6..12d8bbb 100644 --- a/source/Config.mc +++ b/source/Config.mc @@ -10,8 +10,7 @@ module Config { const RETENTION_SEC = 7 * 24 * 60 * 60; // --- GPS ------------------------------------------------------------ - const GPS_TIMEOUT_MS = 10000; - const GPS_TARGET_ACCURACY_M = 5; + const GPS_TIMEOUT_MS = 30000; // --- Menu ring ----------------------------------------------------- // Angle (degrees) at which the selected icon sits relative to the diff --git a/source/Event.mc b/source/Event.mc index 73e3968..44eda88 100644 --- a/source/Event.mc +++ b/source/Event.mc @@ -10,6 +10,7 @@ class Event { public var lon as Float or Null; public var accuracy as Float or Null; public var address as String or Null; + public var zip as String or Null; function initialize(type as String, timestamp as Number, lat as Float or Null, lon as Float or Null, @@ -20,6 +21,7 @@ class Event { self.lon = lon; self.accuracy = accuracy; self.address = null; + self.zip = null; } function toDict() as Dictionary { @@ -30,9 +32,8 @@ class Event { "lon" => lon, "acc" => accuracy }; - if (address != null) { - d["addr"] = address; - } + if (address != null) { d["addr"] = address; } + if (zip != null) { d["zip"] = zip; } return d; } @@ -44,9 +45,8 @@ class Event { d["lon"] as Float or Null, d["acc"] as Float or Null ); - if (d.hasKey("addr")) { - evt.address = d["addr"] as String or Null; - } + if (d.hasKey("addr")) { evt.address = d["addr"] as String or Null; } + if (d.hasKey("zip")) { evt.zip = d["zip"] as String or Null; } return evt; } } diff --git a/source/GeocodingService.mc b/source/GeocodingService.mc index 9ca0bb1..8afee70 100644 --- a/source/GeocodingService.mc +++ b/source/GeocodingService.mc @@ -2,15 +2,15 @@ import Toybox.Communications; import Toybox.Lang; // Reverse-geocodes a lat/lon pair via Photon API. Calls back with -// a formatted address string or null on failure. +// a Dictionary { "addr" => String, "zip" => String } or null. class GeocodingService { - private var _callback as Method(address as String or Null) as Void; + private var _callback as Method(result as Dictionary or Null) as Void; private var _lat as Float; private var _lon as Float; function initialize(lat as Float, lon as Float, - callback as Method(address as String or Null) as Void) { + callback as Method(result as Dictionary or Null) as Void) { _callback = callback; _lat = lat; _lon = lon; @@ -28,16 +28,13 @@ class GeocodingService { } function _onResponse(responseCode as Number, data as Dictionary or String or Null) as Void { - System.println("GEO: responseCode=" + responseCode); if (responseCode != 200 || data == null || !(data instanceof Dictionary)) { - System.println("GEO: bad response, data=" + data); _callback.invoke(null); return; } var features = data["features"]; if (features == null || !(features instanceof Array) || features.size() == 0) { - System.println("GEO: no features"); _callback.invoke(null); return; } @@ -45,7 +42,6 @@ class GeocodingService { var feature = features[0] as Dictionary; var props = feature["properties"] as Dictionary; var geometry = feature["geometry"] as Dictionary; - System.println("GEO: props=" + props); // Haversine check: only use address if result is within threshold. if (geometry != null) { @@ -54,31 +50,38 @@ class GeocodingService { var rLon = (coords[0] as Double).toFloat(); var rLat = (coords[1] as Double).toFloat(); var dist = Haversine.distance(_lat, _lon, rLat, rLon); - System.println("GEO: dist=" + dist + " max=" + Config.ADDRESS_MAX_DISTANCE_M); if (dist > Config.ADDRESS_MAX_DISTANCE_M) { - System.println("GEO: too far, rejecting"); _callback.invoke(null); return; } } } - var addr = _formatAddress(props); - System.println("GEO: address=" + addr); - _callback.invoke(addr); - } - - private function _formatAddress(props as Dictionary) as String { var street = props["street"]; - var number = props["housenumber"]; - - var parts = ""; - if (street != null) { - parts = street as String; - if (number != null) { - parts = parts + " " + number; - } + if (street == null) { street = props["name"]; } + if (street == null) { + _callback.invoke(null); + return; } - return parts.equals("") ? "Unbekannt" : parts; + + var addr = street as String; + var number = props["housenumber"]; + if (number != null) { + addr = addr + " " + number; + } + + var result = { "addr" => addr } as Dictionary; + var zip = props["postcode"]; + var city = props["city"]; + if (zip != null || city != null) { + var zipCity = ""; + if (zip != null) { zipCity = zip as String; } + if (city != null) { + if (!zipCity.equals("")) { zipCity = zipCity + " "; } + zipCity = zipCity + city; + } + result["zip"] = zipCity; + } + _callback.invoke(result); } } diff --git a/source/GpsService.mc b/source/GpsService.mc index 6631f70..7101556 100644 --- a/source/GpsService.mc +++ b/source/GpsService.mc @@ -47,17 +47,16 @@ class GpsService { if (info == null || info.position == null) { return; } var degrees = info.position.toDegrees(); - var acc = (info.accuracy != null) ? info.accuracy : null; + var quality = (info.accuracy != null) ? info.accuracy : 0; - _bestFix = { - "lat" => degrees[0].toFloat(), - "lon" => degrees[1].toFloat(), - "acc" => (acc != null) ? acc.toFloat() : null - }; - - // Good enough → stop early. - if (acc != null && acc <= Config.GPS_TARGET_ACCURACY_M) { - _finish(_bestFix); + // Always keep the best quality fix. + if (_bestFix == null || quality >= (_bestFix["q"] as Number)) { + _bestFix = { + "lat" => degrees[0].toFloat(), + "lon" => degrees[1].toFloat(), + "acc" => quality.toFloat(), + "q" => quality + }; } } diff --git a/source/HistoryView.mc b/source/HistoryView.mc index 881c8d3..ea39d05 100644 --- a/source/HistoryView.mc +++ b/source/HistoryView.mc @@ -64,7 +64,7 @@ class HistoryView extends WatchUi.View { var evt = _events[_index]; // Use cached address from event if available. if (evt.address != null) { - _addressCache[_index] = evt.address; + _addressCache[_index] = { "addr" => evt.address, "zip" => evt.zip }; return; } if (evt.lat == null || evt.lon == null) { return; } @@ -76,13 +76,14 @@ class HistoryView extends WatchUi.View { _geocoder.start(); } - function _onAddress(address as String or Null) as Void { - _addressCache[_index] = address; + function _onAddress(result as Dictionary or Null) as Void { + _addressCache[_index] = result; _addressLoading = false; - // Persist address in the event so the glance can show it. - if (address != null && _index < _events.size()) { + // Persist in the event so the glance can show it. + if (result != null && _index < _events.size()) { var evt = _events[_index]; - evt.address = address; + evt.address = result["addr"] as String or Null; + evt.zip = result["zip"] as String or Null; EventStore.updateAt(_index, evt); } WatchUi.requestUpdate(); @@ -134,51 +135,51 @@ class HistoryView extends WatchUi.View { private function _drawMiddleSection(dc as Dc, cx as Number, topH as Number, botY as Number) as Void { var evt = _events[_index]; - var midY = topH + (botY - topH) / 2; + var fontDetail = Graphics.FONT_XTINY; + var lineH = dc.getFontHeight(fontDetail); + var sectionMid = topH + (botY - topH) / 2; - // Event type label. - var label = _eventLabel(evt.type); - var color = _eventColor(evt.type); - dc.setColor(color, Config.COLOR_BG); - var fontLabel = Graphics.FONT_SMALL; - var lineH = dc.getFontHeight(fontLabel); - dc.drawText(cx, midY - lineH * 2, fontLabel, label, - Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER); + // Collect lines to draw. + var lines = [] as Array; // [text, color] - // Date + time. - dc.setColor(Config.COLOR_FG, Config.COLOR_BG); - var fontDetail = Graphics.FONT_TINY; - var dateStr = _formatTimestamp(evt.timestamp); - dc.drawText(cx, midY - lineH / 2, fontDetail, dateStr, - Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER); + // 1: Event type (colored). + lines.add([_eventLabel(evt.type), _eventColor(evt.type)]); - // Address / coordinates / status. - var locStr = _locationString(evt); - dc.drawText(cx, midY + lineH / 2, fontDetail, locStr, - Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER); + // 2: Date + time. + lines.add([_formatTimestamp(evt.timestamp), Config.COLOR_FG]); - // Counter (e.g. "3/7"). - dc.setColor(0x888888, Config.COLOR_BG); - dc.drawText(cx, midY + lineH * 3 / 2, Graphics.FONT_XTINY, - (_index + 1) + "/" + _events.size(), - Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER); - } - - private function _locationString(evt as Event) as String { + // 3: Address or status. if (evt.lat == null || evt.lon == null) { - return WatchUi.loadResource(Rez.Strings.history_no_gps) as String; - } - if (_addressCache.hasKey(_index)) { - var cached = _addressCache[_index]; + 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) { - return cached as String; + lines.add([cached["addr"] as String, Config.COLOR_FG]); + var zip = cached["zip"] as String or Null; + if (zip != null) { + lines.add([zip, 0x888888]); + } } - return _formatCoords(evt.lat as Float, evt.lon as Float); + } else if (_addressLoading) { + lines.add([WatchUi.loadResource(Rez.Strings.history_loading_address) as String, 0x888888]); } - if (_addressLoading) { - return WatchUi.loadResource(Rez.Strings.history_loading_address) as String; + + // 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); } - return _formatCoords(evt.lat as Float, evt.lon as Float); } private function _formatCoords(lat as Float, lon as Float) as String { diff --git a/source/MenuView.mc b/source/MenuView.mc index adbb1fd..a11bc09 100644 --- a/source/MenuView.mc +++ b/source/MenuView.mc @@ -67,6 +67,11 @@ class MenuView extends WatchUi.View { var isSelected = (i == _selectedIndex); var targetSize = isSelected ? selSize : baseSize; + // Colored circle behind icon. + var color = _items[i][:color] as Number; + dc.setColor(color, Config.COLOR_BG); + dc.fillCircle(x, y, targetSize / 2); + var iconId = _items[i][:icon]; var bmp = _bitmaps[iconId] as BitmapResource; if (bmp != null) { @@ -91,12 +96,12 @@ class MenuView extends WatchUi.View { cx as Number, cy as Number, targetSize as Number) as Void { var bmpW = bmp.getWidth(); + var bmpH = bmp.getHeight(); var scale = targetSize.toFloat() / bmpW.toFloat(); - dc.drawBitmap2( - cx - targetSize / 2, - cy - targetSize / 2, - bmp, - { :scaleX => scale, :scaleY => scale } - ); + var tf = new Graphics.AffineTransform(); + tf.translate(cx.toFloat(), cy.toFloat()); + tf.scale(scale, scale); + tf.translate(-bmpW.toFloat() / 2.0, -bmpH.toFloat() / 2.0); + dc.drawBitmap2(0, 0, bmp, { :transform => tf }); } }