diff --git a/work_trace/models/wt_location_log.py b/work_trace/models/wt_location_log.py index 2a887dfa..5bc85007 100644 --- a/work_trace/models/wt_location_log.py +++ b/work_trace/models/wt_location_log.py @@ -2,42 +2,62 @@ from odoo import models, fields, api from math import radians, sin, cos, sqrt, atan2 +# Maps Google/OSM activity types to human-readable categories +CATEGORY_MAP = { + 'IN_PASSENGER_VEHICLE': 'In Vehicle', + 'IN_VEHICLE': 'In Vehicle', + 'IN_ROAD_VEHICLE': 'In Vehicle', + 'IN_RAIL_VEHICLE': 'Rail / Transit', + 'IN_TWO_WHEELER_VEHICLE': 'Motorcycle / Scooter', + 'WALKING': 'Walking', + 'ON_FOOT': 'Walking', + 'RUNNING': 'Running', + 'ON_BICYCLE': 'Cycling', + 'STILL': 'Stationary', + 'UNKNOWN': 'Unknown', +} + + class WtLocationLog(models.Model): _name = 'wt.location.log' _description = 'WorkTrace Location Log' _order = 'arrived_at asc' name = fields.Char( - string='Location', + string='Name', compute='_compute_name', store=True ) date = fields.Date(string='Date', required=True, index=True) - arrived_at = fields.Datetime(string='Arrived At', required=True) - departed_at = fields.Datetime(string='Departed At') + arrived_at = fields.Datetime(string='Begin Time', required=True) + departed_at = fields.Datetime(string='End Time') latitude = fields.Float(string='Latitude', digits=(10, 7)) longitude = fields.Float(string='Longitude', digits=(10, 7)) address = fields.Char(string='Address') place_name = fields.Char(string='Place / Business Name') + # Category: OSM-derived for stops, activity-type for travel segments + category = fields.Char(string='Category') + time_at_location = fields.Float( - string='Time at Location (hrs)', + string='Duration (hrs)', compute='_compute_time_at_location', store=True ) distance_from_previous = fields.Float( - string='Distance from Previous (mi)', + string='Distance (mi)', digits=(10, 2) ) travel_time_from_previous = fields.Float( - string='Travel Time from Previous (hrs)', + string='Travel Time (hrs)', digits=(10, 2) ) travel_mode = fields.Selection([ ('driving', 'Driving'), ('walking', 'Walking'), ('transit', 'Transit'), + ('cycling', 'Cycling'), ('unknown', 'Unknown'), ], string='Travel Mode', default='unknown') @@ -54,7 +74,7 @@ class WtLocationLog(models.Model): add_to_calendar = fields.Boolean(string='Add to Calendar', default=True) notes = fields.Text(string='Notes') - @api.depends('address', 'place_name', 'latitude', 'longitude') + @api.depends('place_name', 'address', 'latitude', 'longitude') def _compute_name(self): for rec in self: rec.name = ( @@ -75,7 +95,7 @@ class WtLocationLog(models.Model): @staticmethod def _haversine_distance(lat1, lon1, lat2, lon2): """Calculate distance in miles between two GPS coordinates.""" - R = 3958.8 # Earth radius in miles + R = 3958.8 lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2]) dlat = lat2 - lat1 dlon = lon2 - lon1 @@ -83,8 +103,9 @@ class WtLocationLog(models.Model): return R * 2 * atan2(sqrt(a), sqrt(1 - a)) def action_geocode(self): - """Resolve coordinates to address using OpenStreetMap Nominatim.""" + """Resolve coordinates to address and category using OpenStreetMap Nominatim.""" import requests + import time for rec in self: if not rec.latitude or not rec.longitude: continue @@ -100,19 +121,40 @@ class WtLocationLog(models.Model): resp = requests.get(url, params=params, headers=headers, timeout=10) resp.raise_for_status() result = resp.json() + addr = result.get('address', {}) + rec.address = result.get('display_name', '') + rec.place_name = ( result.get('name') - or result.get('address', {}).get('amenity') - or result.get('address', {}).get('shop') - or result.get('address', {}).get('building') + or addr.get('amenity') + or addr.get('shop') + or addr.get('building') + or addr.get('office') + or addr.get('tourism') or '' ) + + # Category: match OSM types to Google Timeline-style categories + osm_type = ( + addr.get('amenity') + or addr.get('shop') + or addr.get('tourism') + or addr.get('leisure') + or addr.get('office') + or addr.get('building') + or result.get('type', '') + or result.get('class', '') + ) + rec.category = osm_type.replace('_', ' ').title() if osm_type else rec.category + + # Nominatim rate limit: 1 request per second + time.sleep(1) except Exception: pass def action_compute_distances(self): - """Compute distances and travel times between sequential location logs for the same date.""" + """Recompute distances and travel times for all logs grouped by date.""" dates = self.mapped('date') for date in set(dates): logs = self.search([('date', '=', date)], order='arrived_at asc')