diff --git a/Gemfile b/Gemfile index ceefd493..323acf81 100644 --- a/Gemfile +++ b/Gemfile @@ -17,6 +17,7 @@ gem 'turbo-rails' gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] gem "importmap-rails" gem "mapkick-rb" +gem "chartkick" gem 'geocoder' gem 'sidekiq' diff --git a/Gemfile.lock b/Gemfile.lock index 25805348..3af5afb6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -83,6 +83,7 @@ GEM msgpack (~> 1.2) builder (3.2.4) byebug (11.1.3) + chartkick (5.0.6) coderay (1.1.3) concurrent-ruby (1.2.3) connection_pool (2.4.1) @@ -323,6 +324,7 @@ PLATFORMS DEPENDENCIES bootsnap + chartkick debug devise dotenv-rails diff --git a/app/controllers/imports_controller.rb b/app/controllers/imports_controller.rb index eebc62eb..a50f6d76 100644 --- a/app/controllers/imports_controller.rb +++ b/app/controllers/imports_controller.rb @@ -18,7 +18,7 @@ class ImportsController < ApplicationController files.each do |file| import = current_user.imports.create( name: file.original_filename, - source: params[:import][:source], + source: params[:import][:source] ) import.file.attach(file) @@ -27,7 +27,7 @@ class ImportsController < ApplicationController redirect_to imports_url, notice: "#{files.size} files are queued to be imported in background", status: :see_other rescue StandardError => e Import.where(user: current_user, name: files.map(&:original_filename)).destroy_all -Rails.logger.debug e.message + flash.now[:error] = e.message redirect_to new_import_path, notice: e.message, status: :unprocessable_entity diff --git a/app/controllers/points_controller.rb b/app/controllers/points_controller.rb index efd8d9ab..9c57fac5 100644 --- a/app/controllers/points_controller.rb +++ b/app/controllers/points_controller.rb @@ -6,12 +6,11 @@ class PointsController < ApplicationController @countries_and_cities = CountriesAndCities.new(@points).call @coordinates = - @points - .pluck(:latitude, :longitude, :battery, :altitude, :timestamp, :velocity, :id) - .map { [_1.to_f, _2.to_f, _3.to_s, _4.to_s, _5.to_s, _6.to_s, _7] } + @points.pluck(:latitude, :longitude, :battery, :altitude, :timestamp, :velocity, :id) + .map { [_1.to_f, _2.to_f, _3.to_s, _4.to_s, _5.to_s, _6.to_s, _7] } @distance = distance - @start_at = Time.at(start_at) - @end_at = Time.at(end_at) + @start_at = Time.zone.at(start_at) + @end_at = Time.zone.at(end_at) end private @@ -23,7 +22,7 @@ class PointsController < ApplicationController end def end_at - return Date.today.end_of_day.to_i if params[:end_at].nil? + return Time.zone.today.end_of_day.to_i if params[:end_at].nil? params[:end_at].to_datetime.to_i end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index df49c727..30ed41cc 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -22,4 +22,8 @@ module ApplicationHelper { start_at: start_at, end_at: end_at } end + + def header_colors + %w[info success warning error accent secondary primary] + end end diff --git a/app/javascript/application.js b/app/javascript/application.js index 8b283c93..c35271fe 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -7,3 +7,5 @@ import "@hotwired/turbo-rails" import "mapkick/bundle" import "leaflet" import "leaflet-providers" +import "chartkick" +import "Chart.bundle" diff --git a/app/models/stat.rb b/app/models/stat.rb index 7fd717d9..81a2f504 100644 --- a/app/models/stat.rb +++ b/app/models/stat.rb @@ -1,5 +1,33 @@ +# frozen_string_literal: true + class Stat < ApplicationRecord validates :year, :month, presence: true belongs_to :user + + def timespan + DateTime.new(year, month).beginning_of_month..DateTime.new(year, month).end_of_month + end + + def distance_by_day + timespan.to_a.map.with_index(1) do |day, index| + beginning_of_day = day.beginning_of_day.to_i + end_of_day = day.end_of_day.to_i + + data = { day: index, distance: 0 } + + # We have to filter by user as well + points = Point.where(timestamp: beginning_of_day..end_of_day) + + points.each_cons(2) do |point1, point2| + distance = Geocoder::Calculations.distance_between( + [point1.latitude, point1.longitude], [point2.latitude, point2.longitude] + ) + + data[:distance] += distance + end + + [data[:day], data[:distance].round(2)] + end + end end diff --git a/app/models/user.rb b/app/models/user.rb index 8372108a..122407a3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -29,4 +29,8 @@ class User < ApplicationRecord def total_cities Stat.where(user: self).pluck(:toponyms).flatten.size end + + def total_reverse_geocoded + points.where.not(country: nil, city: nil).count + end end diff --git a/app/services/create_stats.rb b/app/services/create_stats.rb index 5a18dcdb..4d1ee1d0 100644 --- a/app/services/create_stats.rb +++ b/app/services/create_stats.rb @@ -16,16 +16,13 @@ class CreateStats end_of_month_timestamp = DateTime.new(year, month).end_of_month.to_i points = points(beginning_of_month_timestamp, end_of_month_timestamp) - next if points.empty? - Stat.create( - year: year, - month: month, - distance: distance(points), - toponyms: toponyms(points), - user: user - ) + stat = Stat.create(year:, month:, user:, distance: distance(points), toponyms: toponyms(points)) + + stat.update(daily_distance: stat.distance_by_day) if stat.persisted? + + stat end end.compact end diff --git a/app/views/shared/_right_sidebar.html.erb b/app/views/shared/_right_sidebar.html.erb index 4d702d10..222d7dbf 100644 --- a/app/views/shared/_right_sidebar.html.erb +++ b/app/views/shared/_right_sidebar.html.erb @@ -7,7 +7,7 @@ <% end %> diff --git a/app/views/stats/_stat.html.erb b/app/views/stats/_stat.html.erb index 9f7ba4e4..0a1bdc20 100644 --- a/app/views/stats/_stat.html.erb +++ b/app/views/stats/_stat.html.erb @@ -1,7 +1,7 @@

- <%= link_to points_url(month_timespan(stat)), class: 'underline hover:no-underline' do %> + <%= link_to points_url(month_timespan(stat)), class: "underline hover:no-underline text-#{header_colors.sample}" do %> <%= "#{Date::MONTHNAMES[stat.month]} of #{stat.year}" %> <% end %>

@@ -10,6 +10,13 @@
<%= stat.toponyms.count %> countries, <%= stat.toponyms.sum { _1['cities'].count } %> cities
+ <%= column_chart( + stat.daily_distance, + height: '100px', + suffix: ' km', + xtitle: 'Days', + ytitle: 'Distance' + ) %> <% end %>
diff --git a/app/views/stats/index.html.erb b/app/views/stats/index.html.erb index 0a2c3990..8ac5ade8 100644 --- a/app/views/stats/index.html.erb +++ b/app/views/stats/index.html.erb @@ -7,10 +7,24 @@
Total distance
+
+
+ <%= number_with_delimiter current_user.points.count %> +
+
Geopoints tracked
+
+ <% if REVERSE_GEOCODING_ENABLED %>
- <%= current_user.total_countries %> + <%= current_user.total_reverse_geocoded %> +
+
Reverse geocoded points
+
+ +
+
+ <%= number_with_delimiter current_user.total_countries %>
Countries visited
diff --git a/config/importmap.rb b/config/importmap.rb index ef94f1a7..695152b9 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -12,3 +12,5 @@ pin_all_from "app/javascript/controllers", under: "controllers" pin "mapkick/bundle", to: "mapkick.bundle.js" pin "leaflet" # @1.9.4 pin "leaflet-providers" # @2.0.0 +pin "chartkick", to: "chartkick.js" +pin "Chart.bundle", to: "Chart.bundle.js" diff --git a/db/migrate/20240324173315_add_daily_distance_to_stat.rb b/db/migrate/20240324173315_add_daily_distance_to_stat.rb new file mode 100644 index 00000000..96b3b25d --- /dev/null +++ b/db/migrate/20240324173315_add_daily_distance_to_stat.rb @@ -0,0 +1,5 @@ +class AddDailyDistanceToStat < ActiveRecord::Migration[7.1] + def change + add_column :stats, :daily_distance, :jsonb, default: {} + end +end diff --git a/db/schema.rb b/db/schema.rb index 353086e1..63e83a12 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_03_24_161800) do +ActiveRecord::Schema[7.1].define(version: 2024_03_24_173315) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -101,6 +101,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_03_24_161800) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.bigint "user_id", null: false + t.jsonb "daily_distance", default: {} t.index ["distance"], name: "index_stats_on_distance" t.index ["month"], name: "index_stats_on_month" t.index ["user_id"], name: "index_stats_on_user_id"