From 5d14b406bd7bc74e82504863098da069fb0da4d2 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Wed, 28 Aug 2024 23:54:00 +0200 Subject: [PATCH 1/4] Add DISTANCE_UNIT environment variable --- .app_version | 2 +- .env.development | 1 + CHANGELOG.md | 8 ++++++++ app/controllers/map_controller.rb | 4 +++- app/helpers/application_helper.rb | 5 +++-- app/models/user.rb | 3 ++- app/services/areas/visits/create.rb | 9 +++++++-- app/services/create_stats.rb | 8 ++++---- app/views/map/index.html.erb | 3 ++- app/views/stats/_stat.html.erb | 4 ++-- app/views/stats/_year.html.erb | 2 +- app/views/stats/index.html.erb | 8 ++++---- config/initializers/00_constants.rb | 1 + config/initializers/geocoder.rb | 2 +- spec/models/user_spec.rb | 4 ++-- 15 files changed, 42 insertions(+), 22 deletions(-) diff --git a/.app_version b/.app_version index 26acbf08..54d1a4f2 100644 --- a/.app_version +++ b/.app_version @@ -1 +1 @@ -0.12.2 +0.13.0 diff --git a/.env.development b/.env.development index 08a48784..3f83ed07 100644 --- a/.env.development +++ b/.env.development @@ -5,3 +5,4 @@ DATABASE_NAME=dawarich_development DATABASE_PORT=5432 REDIS_URL=redis://localhost:6379/1 PHOTON_API_HOST='photon.komoot.io' +DISTANCE_UNIT='mi' diff --git a/CHANGELOG.md b/CHANGELOG.md index 99feeae7..f59c7409 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.13.0] — 2024-08-28 + +### Added + +- Support for miles. To switch to miles, provide `DISTANCE_UNIT` environment variable with value `mi` in the `docker-compose.yml` file. Default value is `km`. + +It's recommended to update your stats manually after changing the `DISTANCE_UNIT` environment variable. You can do this by clicking the "Update stats" button on the Stats page. + ## [0.12.2] — 2024-08-28 ### Added diff --git a/app/controllers/map_controller.rb b/app/controllers/map_controller.rb index be255f08..0dfef36b 100644 --- a/app/controllers/map_controller.rb +++ b/app/controllers/map_controller.rb @@ -35,7 +35,9 @@ class MapController < ApplicationController @distance ||= 0 @coordinates.each_cons(2) do - @distance += Geocoder::Calculations.distance_between([_1[0], _1[1]], [_2[0], _2[1]], units: :km) + @distance += Geocoder::Calculations.distance_between( + [_1[0], _1[1]], [_2[0], _2[1]], units: DISTANCE_UNIT.to_sym + ) end @distance.round(1) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f0531cc0..fd5b86e2 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -50,7 +50,8 @@ module ApplicationHelper "#{countries} countries, #{cities} cities" end - def year_distance_stat_in_km(year, user) + def year_distance_stat(year, user) + # In km or miles, depending on the application settings (DISTANCE_UNIT) Stat.year_distance(year, user).sum { _1[1] } end @@ -81,7 +82,7 @@ module ApplicationHelper def sidebar_distance(distance) return unless distance - "#{distance} km" + "#{distance} #{DISTANCE_UNIT}" end def sidebar_points(points) diff --git a/app/models/user.rb b/app/models/user.rb index a7241596..806b3b2f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -35,7 +35,8 @@ class User < ApplicationRecord .compact end - def total_km + def total_distance + # In km or miles, depending on the application settings (DISTANCE_UNIT) stats.sum(:distance) end diff --git a/app/services/areas/visits/create.rb b/app/services/areas/visits/create.rb index 5bec87c6..115525bd 100644 --- a/app/services/areas/visits/create.rb +++ b/app/services/areas/visits/create.rb @@ -30,10 +30,15 @@ class Areas::Visits::Create end def area_points(area) - area_radius_in_km = area.radius / 1000.0 + area_radius = + if DISTANCE_UNIT.to_sym == :km + area.radius / 1000.0 + else + area.radius / 1609.344 + end points = Point.where(user_id: user.id) - .near([area.latitude, area.longitude], area_radius_in_km) + .near([area.latitude, area.longitude], area_radius, units: DISTANCE_UNIT.to_sym) .order(timestamp: :asc) # check if all points within the area are assigned to a visit diff --git a/app/services/create_stats.rb b/app/services/create_stats.rb index 92e903d7..b245ee0a 100644 --- a/app/services/create_stats.rb +++ b/app/services/create_stats.rb @@ -50,15 +50,15 @@ class CreateStats end def distance(points) - km = 0 + distance = 0 points.each_cons(2) do - km += Geocoder::Calculations.distance_between( - [_1.latitude, _1.longitude], [_2.latitude, _2.longitude], units: :km + distance += Geocoder::Calculations.distance_between( + [_1.latitude, _1.longitude], [_2.latitude, _2.longitude], units: DISTANCE_UNIT.to_sym ) end - km + distance end def toponyms(points) diff --git a/app/views/map/index.html.erb b/app/views/map/index.html.erb index d01c0041..3db424f9 100644 --- a/app/views/map/index.html.erb +++ b/app/views/map/index.html.erb @@ -41,9 +41,10 @@
- data-controller="maps" data-coordinates="<%= @coordinates %>" data-timezone="<%= Rails.configuration.time_zone %>">
diff --git a/app/views/stats/_stat.html.erb b/app/views/stats/_stat.html.erb index d0ab9074..4309c3b0 100644 --- a/app/views/stats/_stat.html.erb +++ b/app/views/stats/_stat.html.erb @@ -5,7 +5,7 @@ <%= "#{Date::MONTHNAMES[stat.month]} of #{stat.year}" %> <% end %> -

<%= stat.distance %>km

+

<%= stat.distance %><%= DISTANCE_UNIT %>

<% if REVERSE_GEOCODING_ENABLED %>
<%= countries_and_cities_stat_for_month(stat) %> @@ -15,7 +15,7 @@ <%= column_chart( stat.daily_distance, height: '100px', - suffix: ' km', + suffix: " #{DISTANCE_UNIT}", xtitle: 'Days', ytitle: 'Distance' ) %> diff --git a/app/views/stats/_year.html.erb b/app/views/stats/_year.html.erb index 8971f3ce..3156a9e9 100644 --- a/app/views/stats/_year.html.erb +++ b/app/views/stats/_year.html.erb @@ -6,7 +6,7 @@ <%= column_chart( Stat.year_distance(year, current_user), height: '200px', - suffix: ' km', + suffix: " #{DISTANCE_UNIT}", xtitle: 'Days', ytitle: 'Distance' ) %> diff --git a/app/views/stats/index.html.erb b/app/views/stats/index.html.erb index e1fa8699..3dce46c4 100644 --- a/app/views/stats/index.html.erb +++ b/app/views/stats/index.html.erb @@ -4,7 +4,7 @@
- <%= number_with_delimiter(current_user.total_km) %> km + <%= number_with_delimiter(current_user.total_distance) %> <%= DISTANCE_UNIT %>
Total distance
@@ -32,8 +32,8 @@ <%= link_to '[Map]', map_url(year_timespan(year)), class: 'underline hover:no-underline' %>

- <% cache [current_user, 'year_distance_stat_in_km', year], skip_digest: true do %> - <%= number_with_delimiter year_distance_stat_in_km(year, current_user) %>km + <% cache [current_user, 'year_distance_stat', year], skip_digest: true do %> + <%= number_with_delimiter year_distance_stat(year, current_user) %><%= DISTANCE_UNIT %> <% end %>

<% if REVERSE_GEOCODING_ENABLED %> @@ -44,7 +44,7 @@ <%= column_chart( Stat.year_distance(year, current_user), height: '200px', - suffix: ' km', + suffix: " #{DISTANCE_UNIT}", xtitle: 'Days', ytitle: 'Distance' ) %> diff --git a/config/initializers/00_constants.rb b/config/initializers/00_constants.rb index 7251d114..198a7472 100644 --- a/config/initializers/00_constants.rb +++ b/config/initializers/00_constants.rb @@ -3,3 +3,4 @@ MIN_MINUTES_SPENT_IN_CITY = ENV.fetch('MIN_MINUTES_SPENT_IN_CITY', 60).to_i REVERSE_GEOCODING_ENABLED = ENV.fetch('REVERSE_GEOCODING_ENABLED', 'true') == 'true' PHOTON_API_HOST = ENV.fetch('PHOTON_API_HOST', nil) +DISTANCE_UNIT = ENV.fetch('DISTANCE_UNIT', 'km') diff --git a/config/initializers/geocoder.rb b/config/initializers/geocoder.rb index 5352049e..7dedd07a 100644 --- a/config/initializers/geocoder.rb +++ b/config/initializers/geocoder.rb @@ -2,7 +2,7 @@ settings = { timeout: 5, - units: :km, + units: DISTANCE_UNIT.to_sym, cache: Redis.new, always_raise: :all, cache_options: { diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index bd7a2c30..25421848 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -50,8 +50,8 @@ RSpec.describe User, type: :model do end end - describe '#total_km' do - subject { user.total_km } + describe '#total_distance' do + subject { user.total_distance } let!(:stat1) { create(:stat, user:, distance: 10) } let!(:stat2) { create(:stat, user:, distance: 20) } From 9f9debdb1d1f874f81f49f604b66d15accd6cc99 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Thu, 29 Aug 2024 00:09:04 +0200 Subject: [PATCH 2/4] Update js files to support miles --- app/javascript/controllers/maps_controller.js | 10 ++++- app/javascript/maps/helpers.js | 39 +++++++++++++++---- docker-compose.yml | 2 + 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index 4a1e82cc..d8e60825 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -25,6 +25,7 @@ export default class extends Controller { this.userSettings = JSON.parse(this.element.dataset.user_settings); this.clearFogRadius = parseInt(this.userSettings.fog_of_war_meters) || 50; this.routeOpacity = parseFloat(this.userSettings.route_opacity) || 0.6; + this.distanceUnit = this.userSettings.distance_unit || "km"; this.center = this.markers[this.markers.length - 1] || [52.514568, 13.350111]; @@ -134,6 +135,13 @@ export default class extends Controller { createPopupContent(marker) { const timezone = this.element.dataset.timezone; + if (this.distanceUnit === "mi") { + // convert marker[5] from km/h to mph + marker[5] = marker[5] * 0.621371; + // convert marker[3] from meters to feet + marker[3] = marker[3] * 3.28084; + } + return ` Timestamp: ${formatDate(marker[4], timezone)}
Latitude: ${marker[0]}
@@ -268,7 +276,7 @@ export default class extends Controller { Start: ${firstTimestamp}
End: ${lastTimestamp}
Duration: ${timeOnRoute}
- Total Distance: ${formatDistance(totalDistance)}
+ Total Distance: ${formatDistance(totalDistance, this.distanceUnit)}
`; if (isDebugMode) { diff --git a/app/javascript/maps/helpers.js b/app/javascript/maps/helpers.js index 4a7d6817..3eb21899 100644 --- a/app/javascript/maps/helpers.js +++ b/app/javascript/maps/helpers.js @@ -1,9 +1,28 @@ // javascript/maps/helpers.js -export function formatDistance(distance) { - if (distance < 1000) { - return `${distance.toFixed(2)} meters`; +export function formatDistance(distance, unit = 'km') { + if (unit === 'mi') { + distance *= 0.621371; // Convert to miles + var smallUnit = 'ft'; + var bigUnit = 'mi'; + + // If the distance is less than 1 mi, return it in feet + // else return it in miles + if (distance < 621) { + distance *= 5280; + + return `${distance.toFixed(2)} ${smallUnit}`; + } } else { - return `${(distance / 1000).toFixed(2)} km`; + var smallUnit = 'm'; + var bigUnit = 'km'; + } + + // If the distance is less than 1 km/mi, return it in meters/feet + + if (distance < 1000) { + return `${distance.toFixed(2)} ${smallUnit}`; + } else { + return `${(distance / 1000).toFixed(2)} ${bigUnit}`; } } @@ -37,9 +56,10 @@ export function formatDate(timestamp, timezone) { return date.toLocaleString("en-GB", { timeZone: timezone }); } -export function haversineDistance(lat1, lon1, lat2, lon2) { +export function haversineDistance(lat1, lon1, lat2, lon2, unit = 'km') { const toRad = (x) => (x * Math.PI) / 180; - const R = 6371; // Radius of the Earth in kilometers + const R_km = 6371; // Radius of the Earth in kilometers + const R_miles = 3959; // Radius of the Earth in miles const dLat = toRad(lat2 - lat1); const dLon = toRad(lon2 - lon1); const a = @@ -47,5 +67,10 @@ export function haversineDistance(lat1, lon1, lat2, lon2) { Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - return R * c * 1000; // Distance in meters + + if (unit === 'miles') { + return R_miles * c; // Distance in miles + } else { + return R_km * c; // Distance in kilometers + } } diff --git a/docker-compose.yml b/docker-compose.yml index 895c0c9f..31f416c6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -49,6 +49,7 @@ services: APPLICATION_HOSTS: localhost TIME_ZONE: Europe/London APPLICATION_PROTOCOL: http + DISTANCE_UNIT: km logging: driver: "json-file" options: @@ -81,6 +82,7 @@ services: APPLICATION_HOSTS: localhost BACKGROUND_PROCESSING_CONCURRENCY: 10 APPLICATION_PROTOCOL: http + DISTANCE_UNIT: km logging: driver: "json-file" options: From 88de60517e289ee86d80a3979e78d390ec13dcc0 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Thu, 29 Aug 2024 00:17:51 +0200 Subject: [PATCH 3/4] Update route popup with the correct distance unit --- CHANGELOG.md | 2 ++ app/javascript/controllers/maps_controller.js | 4 +-- app/javascript/maps/helpers.js | 36 ++++++++++--------- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f59c7409..dbc9cd05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). It's recommended to update your stats manually after changing the `DISTANCE_UNIT` environment variable. You can do this by clicking the "Update stats" button on the Stats page. +⚠️IMPORTANT⚠️: All settings are still should be provided in meters. All calculations though will be converted to feets and miles if `DISTANCE_UNIT` is set to `mi`. + ## [0.12.2] — 2024-08-28 ### Added diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index d8e60825..96bb9b92 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -25,7 +25,7 @@ export default class extends Controller { this.userSettings = JSON.parse(this.element.dataset.user_settings); this.clearFogRadius = parseInt(this.userSettings.fog_of_war_meters) || 50; this.routeOpacity = parseFloat(this.userSettings.route_opacity) || 0.6; - this.distanceUnit = this.userSettings.distance_unit || "km"; + this.distanceUnit = this.element.dataset.distance_unit || "km"; this.center = this.markers[this.markers.length - 1] || [52.514568, 13.350111]; @@ -278,7 +278,7 @@ export default class extends Controller { Duration: ${timeOnRoute}
Total Distance: ${formatDistance(totalDistance, this.distanceUnit)}
`; - +console.log(this.distanceUnit); if (isDebugMode) { const prevPoint = polylineCoordinates[0]; const nextPoint = polylineCoordinates[polylineCoordinates.length - 1]; diff --git a/app/javascript/maps/helpers.js b/app/javascript/maps/helpers.js index 3eb21899..7aef91c3 100644 --- a/app/javascript/maps/helpers.js +++ b/app/javascript/maps/helpers.js @@ -1,28 +1,30 @@ // javascript/maps/helpers.js export function formatDistance(distance, unit = 'km') { + let smallUnit, bigUnit; + if (unit === 'mi') { - distance *= 0.621371; // Convert to miles - var smallUnit = 'ft'; - var bigUnit = 'mi'; - - // If the distance is less than 1 mi, return it in feet - // else return it in miles - if (distance < 621) { - distance *= 5280; + distance *= 0.621371; // Convert km to miles + smallUnit = 'ft'; + bigUnit = 'mi'; + // If the distance is less than 1 mile, return it in feet + if (distance < 1) { + distance *= 5280; // Convert miles to feet return `${distance.toFixed(2)} ${smallUnit}`; + } else { + return `${distance.toFixed(2)} ${bigUnit}`; } } else { - var smallUnit = 'm'; - var bigUnit = 'km'; - } + smallUnit = 'm'; + bigUnit = 'km'; - // If the distance is less than 1 km/mi, return it in meters/feet - - if (distance < 1000) { - return `${distance.toFixed(2)} ${smallUnit}`; - } else { - return `${(distance / 1000).toFixed(2)} ${bigUnit}`; + // If the distance is less than 1 km, return it in meters + if (distance < 1) { + distance *= 1000; // Convert km to meters + return `${distance.toFixed(2)} ${smallUnit}`; + } else { + return `${distance.toFixed(2)} ${bigUnit}`; + } } } From 662eae81fd2b25127b2d6b85b9193bfd87f211f2 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Fri, 6 Sep 2024 21:22:13 +0200 Subject: [PATCH 4/4] Add miles tests and refactor CreateStats service a bit --- CHANGELOG.md | 2 +- app/services/create_stats.rb | 53 +++++++++++++--------- spec/services/create_stats_spec.rb | 72 ++++++++++++++++++++++-------- 3 files changed, 87 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbc9cd05..93946468 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [0.13.0] — 2024-08-28 +## [0.13.3] — 2024-09-6 ### Added diff --git a/app/services/create_stats.rb b/app/services/create_stats.rb index b245ee0a..fc086d69 100644 --- a/app/services/create_stats.rb +++ b/app/services/create_stats.rb @@ -12,29 +12,12 @@ class CreateStats def call users.each do |user| years.each do |year| - months.each do |month| - beginning_of_month_timestamp = DateTime.new(year, month).beginning_of_month.to_i - end_of_month_timestamp = DateTime.new(year, month).end_of_month.to_i - - points = points(user, beginning_of_month_timestamp, end_of_month_timestamp) - next if points.empty? - - stat = Stat.find_or_initialize_by(year:, month:, user:) - stat.distance = distance(points) - stat.toponyms = toponyms(points) - stat.daily_distance = stat.distance_by_day - stat.save - end + months.each { |month| update_month_stats(user, year, month) } end - Notifications::Create.new(user:, kind: :info, title: 'Stats updated', content: 'Stats updated').call + create_stats_updated_notification(user) rescue StandardError => e - Notifications::Create.new( - user:, - kind: :error, - title: 'Stats update failed', - content: "#{e.message}, stacktrace: #{e.backtrace.join("\n")}" - ).call + create_stats_update_failed_notification(user, e) end end @@ -49,6 +32,21 @@ class CreateStats .select(:latitude, :longitude, :timestamp, :city, :country) end + def update_month_stats(user, year, month) + beginning_of_month_timestamp = DateTime.new(year, month).beginning_of_month.to_i + end_of_month_timestamp = DateTime.new(year, month).end_of_month.to_i + + points = points(user, beginning_of_month_timestamp, end_of_month_timestamp) + + return if points.empty? + + stat = Stat.find_or_initialize_by(year:, month:, user:) + stat.distance = distance(points) + stat.toponyms = toponyms(points) + stat.daily_distance = stat.distance_by_day + stat.save + end + def distance(points) distance = 0 @@ -64,4 +62,19 @@ class CreateStats def toponyms(points) CountriesAndCities.new(points).call end + + def create_stats_updated_notification(user) + Notifications::Create.new( + user:, kind: :info, title: 'Stats updated', content: 'Stats updated' + ).call + end + + def create_stats_update_failed_notification(user, error) + Notifications::Create.new( + user:, + kind: :error, + title: 'Stats update failed', + content: "#{error.message}, stacktrace: #{error.backtrace.join("\n")}" + ).call + end end diff --git a/spec/services/create_stats_spec.rb b/spec/services/create_stats_spec.rb index 95b9f23f..9bd46728 100644 --- a/spec/services/create_stats_spec.rb +++ b/spec/services/create_stats_spec.rb @@ -21,32 +21,66 @@ RSpec.describe CreateStats do let!(:point2) { create(:point, user:, import:, latitude: 1, longitude: 2) } let!(:point3) { create(:point, user:, import:, latitude: 3, longitude: 4) } - it 'creates stats' do - expect { create_stats }.to change { Stat.count }.by(1) - end - - it 'calculates distance' do - create_stats - - expect(Stat.last.distance).to eq(563) - end - - it 'created notifications' do - expect { create_stats }.to change { Notification.count }.by(1) - end - - context 'when there is an error' do - before do - allow(Stat).to receive(:find_or_initialize_by).and_raise(StandardError) + context 'when units are kilometers' do + it 'creates stats' do + expect { create_stats }.to change { Stat.count }.by(1) end - it 'does not create stats' do - expect { create_stats }.not_to(change { Stat.count }) + it 'calculates distance' do + create_stats + + expect(Stat.last.distance).to eq(563) end it 'created notifications' do expect { create_stats }.to change { Notification.count }.by(1) end + + context 'when there is an error' do + before do + allow(Stat).to receive(:find_or_initialize_by).and_raise(StandardError) + end + + it 'does not create stats' do + expect { create_stats }.not_to(change { Stat.count }) + end + + it 'created notifications' do + expect { create_stats }.to change { Notification.count }.by(1) + end + end + end + + context 'when units are miles' do + before { stub_const('DISTANCE_UNIT', 'mi') } + + it 'creates stats' do + expect { create_stats }.to change { Stat.count }.by(1) + end + + it 'calculates distance' do + create_stats + + expect(Stat.last.distance).to eq(349) + end + + it 'created notifications' do + expect { create_stats }.to change { Notification.count }.by(1) + end + + context 'when there is an error' do + before do + allow(Stat).to receive(:find_or_initialize_by).and_raise(StandardError) + end + + it 'does not create stats' do + expect { create_stats }.not_to(change { Stat.count }) + end + + it 'created notifications' do + expect { create_stats }.to change { Notification.count }.by(1) + end + end end end end