Skip to content

Commit

Permalink
Merge pull request #22 from Freika/fix/city-detection
Browse files Browse the repository at this point in the history
Rework calculation of cities visited
  • Loading branch information
Freika authored May 5, 2024
2 parents b293081 + ce7b391 commit 16c2703
Show file tree
Hide file tree
Showing 22 changed files with 240 additions and 91 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ 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.2.0] — 2024-05-05

*Breaking changes:*

This release changes how Dawarich handles a city visit threshold. Previously, the `MINIMUM_POINTS_IN_CITY` environment variable was used to determine the minimum *number of points* in a city to consider it as visited. Now, the `MIN_MINUTES_SPENT_IN_CITY` environment variable is used to determine the minimum *minutes* between two points to consider them as visited the same city.

The logic behind this is the following: if you have a lot of points in a city, it doesn't mean you've spent a lot of time there, especially if your OwnTracks app was in "Move" mode. So, it's better to consider the time spent in a city rather than the number of points.

In your docker-compose.yml file, you need to replace the `MINIMUM_POINTS_IN_CITY` environment variable with `MIN_MINUTES_SPENT_IN_CITY`. The default value is `60`, in minutes.

## [0.1.9] — 2024-04-25

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Copy the contents of the `docker-compose.yml` file to your server and run `docke
## Environment variables

```
MINIMUM_POINTS_IN_CITY — minimum number of points in a city to consider it as a city visited, eg. `10`
MIN_MINUTES_SPENT_IN_CITY — minimum minutes between two points to consider them as visited the same city, e.g. `60`
MAP_CENTER — default map center, e.g. `55.7558,37.6176`
TIME_ZONE — time zone, e.g. `Europe/Berlin`
APPLICATION_HOST — host of the application, e.g. `localhost` or `dawarich.example.com`
Expand Down
2 changes: 1 addition & 1 deletion app/assets/builds/tailwind.css

Large diffs are not rendered by default.

17 changes: 12 additions & 5 deletions app/controllers/export_controller.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

class ExportController < ApplicationController
before_action :authenticate_user!

Expand All @@ -6,12 +8,17 @@ def index
end

def download
first_point_datetime = Time.at(current_user.points.first.timestamp).to_s
last_point_datetime = Time.at(current_user.points.last.timestamp).to_s
filename = "dawarich-export-#{first_point_datetime}-#{last_point_datetime}.json".gsub(' ', '_')

export = current_user.export_data

send_data export, filename: filename
send_data export, filename:
end

private

def filename
first_point_datetime = Time.zone.at(current_user.points.first.timestamp).to_s
last_point_datetime = Time.zone.at(current_user.points.last.timestamp).to_s

"dawarich-export-#{first_point_datetime}-#{last_point_datetime}.json".gsub(' ', '_')
end
end
2 changes: 1 addition & 1 deletion app/jobs/import_job.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

class ImportJob < ApplicationJob
queue_as :default
queue_as :imports

def perform(user_id, import_id)
user = User.find(user_id)
Expand Down
4 changes: 3 additions & 1 deletion app/jobs/reverse_geocoding_job.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

class ReverseGeocodingJob < ApplicationJob
queue_as :low
queue_as :reverse_geocoding

def perform(point_id)
return unless REVERSE_GEOCODING_ENABLED
Expand Down
2 changes: 1 addition & 1 deletion app/jobs/stat_creating_job.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

class StatCreatingJob < ApplicationJob
queue_as :default
queue_as :stats

def perform(user_ids = nil)
CreateStats.new(user_ids).call
Expand Down
5 changes: 4 additions & 1 deletion app/models/stat.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ def self.year_cities_and_countries(year)

data = CountriesAndCities.new(points).call

{ countries: data.map { _1[:country] }.uniq.count, cities: data.sum { |country| country[:cities].count } }
{
countries: data.map { _1[:country] }.uniq.count,
cities: data.sum { |country| country[:cities].count }
}
end

def self.years
Expand Down
29 changes: 24 additions & 5 deletions app/models/user.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
Expand All @@ -6,24 +8,41 @@ class User < ApplicationRecord

has_many :imports, dependent: :destroy
has_many :points, through: :imports
has_many :stats
has_many :stats, dependent: :destroy

after_create :create_api_key

def export_data
::ExportSerializer.new(points, self.email).call
::ExportSerializer.new(points, email).call
end

def countries_visited
stats.pluck(:toponyms).flatten.map { _1['country'] }.uniq.compact
end

def cities_visited
stats
.where.not(toponyms: nil)
.pluck(:toponyms)
.flatten
.reject { |toponym| toponym['cities'].blank? }
.pluck('cities')
.flatten
.pluck('city')
.uniq
.compact
end

def total_km
Stat.where(user: self).sum(:distance)
stats.sum(:distance)
end

def total_countries
Stat.where(user: self).pluck(:toponyms).flatten.map { _1['country'] }.uniq.size
countries_visited.size
end

def total_cities
Stat.where(user: self).pluck(:toponyms).flatten.size
cities_visited.size
end

def total_reverse_geocoded
Expand Down
16 changes: 8 additions & 8 deletions app/services/countries_and_cities.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,31 +27,31 @@ def map_with_cities(grouped_records)
grouped_points
.pluck(:city, :timestamp) # Extract city and timestamp
.delete_if { _1.first.nil? } # Remove records without city
.group_by { |city, _| city }
.group_by { |city, _| city } # Group by city
.transform_values do |cities|
{
points: cities.count,
timestamp: cities.map(&:last).max # Get the maximum timestamp
last_timestamp: cities.map(&:last).max, # Get the maximum timestamp
stayed_for: ((cities.map(&:last).max - cities.map(&:last).min).to_i / 60) # Calculate the time stayed in minutes
}
end
end
end

def filter_cities(mapped_with_cities)
# In future, we would want to remove cities where user spent less than
# 1 hour per day

# Remove cities with less than MINIMUM_POINTS_IN_CITY
# Remove cities where user stayed for less than 1 hour
mapped_with_cities.transform_values do |cities|
cities.reject { |_, data| data[:points] < MINIMUM_POINTS_IN_CITY }
cities.reject { |_, data| data[:stayed_for] < MIN_MINUTES_SPENT_IN_CITY }
end
end

def normalize_result(hash)
hash.map do |country, cities|
{
country:,
cities: cities.map { |city, data| { city:, points: data[:points], timestamp: data[:timestamp] } }
cities: cities.map do |city, data|
{ city:, points: data[:points], timestamp: data[:last_timestamp], stayed_for: data[:stayed_for]}
end
}
end
end
Expand Down
4 changes: 1 addition & 3 deletions app/services/create_stats.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,11 @@ def call
points = points(beginning_of_month_timestamp, end_of_month_timestamp)
next if points.empty?

stat = Stat.find_or_initialize_by(year: year, month: month, user: user)
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

stat
end
end
end
Expand Down
3 changes: 0 additions & 3 deletions app/views/export/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,4 @@
<h1 class='text-3xl font-bold'>Export Data</h1>
<%= link_to 'Download JSON', export_download_path, class: 'btn btn-primary my-5' %>
</div>
<div class="mockup-code p-5">
<code><%= current_user.export_data %></code>
</div>
</div>
6 changes: 5 additions & 1 deletion app/views/shared/_right_sidebar.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,16 @@
<% if REVERSE_GEOCODING_ENABLED && @countries_and_cities&.any? %>
<hr class='my-5'>
<% @countries_and_cities.each do |country| %>
<% next if country[:cities].empty? %>

<h2 class="text-lg font-semibold mt-5">
<%= country[:country] %> (<%= country[:cities].count %> cities)
</h2>
<ul>
<% country[:cities].each do |city| %>
<li><%= city[:city] %> (<%= Time.zone.at(city[:timestamp]).strftime("%d.%m.%Y") %>)</li>
<li>
<%= city[:city] %> (<%= Time.zone.at(city[:timestamp]).strftime("%d.%m.%Y") %>)
</li>
<% end %>
</ul>
<% end %>
Expand Down
31 changes: 29 additions & 2 deletions app/views/stats/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,44 @@
</div>

<div class="stat text-center">
<div class="stat-value text-warning">
<div class="stat-value text-warning underline hover:no-underline hover:cursor-pointer" onclick="countries_visited.showModal()">
<%= number_with_delimiter current_user.total_countries %>
</div>
<div class="stat-title">Countries visited</div>

<dialog id="countries_visited" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg">Countries visited</h3>
<p class="py-4">
<% current_user.countries_visited.each do |country| %>
<p><%= country %></p>
<% end %>
</p>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
</div>

<div class="stat text-center">
<div class="stat-value">
<div class="stat-value hover:cursor-pointer hover:no-underline underline" onclick="cities_visited.showModal()">
<%= current_user.total_cities %>
</div>
<div class="stat-title">Cities visited</div>
<dialog id="cities_visited" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg">Countries visited</h3>
<p class="py-4">
<% current_user.cities_visited.each do |city| %>
<p><%= city %></p>
<% end %>
</p>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
</div>
<% end %>
</div>
Expand Down
8 changes: 5 additions & 3 deletions config/application.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
require_relative "boot"
# frozen_string_literal: true

require "rails/all"
require_relative 'boot'

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Expand All @@ -14,7 +16,7 @@ class Application < Rails::Application
# Please, add to the `ignore` list any other `lib` subdirectories that do
# not contain `.rb` files, or that should not be reloaded or eager loaded.
# Common ones are `templates`, `generators`, or `middleware`, for example.
config.autoload_lib(ignore: %w(assets tasks))
config.autoload_lib(ignore: %w[assets tasks])

# Configuration for the application, engines, and railties goes here.
#
Expand Down
2 changes: 1 addition & 1 deletion config/initializers/00_constants.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

MINIMUM_POINTS_IN_CITY = ENV.fetch('MINIMUM_POINTS_IN_CITY', 5).to_i
MIN_MINUTES_SPENT_IN_CITY = ENV.fetch('MIN_MINUTES_SPENT_IN_CITY', 60).to_i
MAP_CENTER = ENV.fetch('MAP_CENTER', '[55.7522, 37.6156]')
REVERSE_GEOCODING_ENABLED = ENV.fetch('REVERSE_GEOCODING_ENABLED', 'true') == 'true'
5 changes: 3 additions & 2 deletions config/sidekiq.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
:queues:
- critical
- default
- low
- imports
- stats
- reverse_geocoding
6 changes: 3 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ services:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
dawarich_app:
image: freikin/dawarich:0.1.6.1
image: freikin/dawarich:latest
container_name: dawarich_app
volumes:
- gem_cache:/usr/local/bundle/gems
Expand All @@ -40,13 +40,13 @@ services:
DATABASE_USERNAME: postgres
DATABASE_PASSWORD: password
DATABASE_NAME: dawarich_development
MINIMUM_POINTS_IN_CITY: 5
MIN_MINUTES_SPENT_IN_CITY: 60
APPLICATION_HOST: localhost
depends_on:
- dawarich_db
- dawarich_redis
dawarich_sidekiq:
image: freikin/dawarich:0.1.6.1
image: freikin/dawarich:latest
container_name: dawarich_sidekiq
volumes:
- gem_cache:/usr/local/bundle/gems
Expand Down
Loading

0 comments on commit 16c2703

Please sign in to comment.