Calculate trip data in the background

This commit is contained in:
Eugene Burmakin 2025-05-15 21:33:01 +02:00
parent 108239f41c
commit 088d8b14c2
13 changed files with 175 additions and 52 deletions

1
.rspec
View file

@ -1 +1,2 @@
--require spec_helper --require spec_helper
--profile

View file

@ -15,6 +15,9 @@ class TripsController < ApplicationController
@trip.photo_previews @trip.photo_previews
end end
@photo_sources = @trip.photo_sources @photo_sources = @trip.photo_sources
# Trigger calculation jobs if data is missing
Trips::CalculateAllJob.perform_later(@trip.id) unless @trip.path.present? && @trip.distance.present? && @trip.visited_countries.present?
end end
def new def new
@ -28,7 +31,7 @@ class TripsController < ApplicationController
@trip = current_user.trips.build(trip_params) @trip = current_user.trips.build(trip_params)
if @trip.save if @trip.save
redirect_to @trip, notice: 'Trip was successfully created.' redirect_to @trip, notice: 'Trip was successfully created. Data is being calculated in the background.'
else else
render :new, status: :unprocessable_entity render :new, status: :unprocessable_entity
end end
@ -36,6 +39,7 @@ class TripsController < ApplicationController
def update def update
if @trip.update(trip_params) if @trip.update(trip_params)
# Only recalculate if date range changed (handled by model callback)
redirect_to @trip, notice: 'Trip was successfully updated.', status: :see_other redirect_to @trip, notice: 'Trip was successfully updated.', status: :see_other
else else
render :edit, status: :unprocessable_entity render :edit, status: :unprocessable_entity

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
class Trips::CalculateAllJob < ApplicationJob
queue_as :default
def perform(trip_id)
Trips::CalculatePathJob.perform_later(trip_id)
Trips::CalculateDistanceJob.perform_later(trip_id)
Trips::CalculateCountriesJob.perform_later(trip_id)
end
end

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
class Trips::CalculateCountriesJob < ApplicationJob
queue_as :default
def perform(trip_id)
trip = Trip.find(trip_id)
trip.calculate_countries
trip.save!
broadcast_update(trip)
end
private
def broadcast_update(trip)
Turbo::StreamsChannel.broadcast_update_to(
"trip_#{trip.id}",
target: "trip_countries",
partial: "trips/countries",
locals: { trip: trip }
)
end
end

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
class Trips::CalculateDistanceJob < ApplicationJob
queue_as :default
def perform(trip_id)
trip = Trip.find(trip_id)
trip.calculate_distance
trip.save!
broadcast_update(trip)
end
private
def broadcast_update(trip)
Turbo::StreamsChannel.broadcast_update_to(
"trip_#{trip.id}",
target: "trip_distance",
partial: "trips/distance",
locals: { trip: trip }
)
end
end

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
class Trips::CalculatePathJob < ApplicationJob
queue_as :default
def perform(trip_id)
trip = Trip.find(trip_id)
trip.calculate_path
trip.save!
broadcast_update(trip)
end
private
def broadcast_update(trip)
Turbo::StreamsChannel.broadcast_update_to(
"trip_#{trip.id}",
target: "trip_path",
partial: "trips/path",
locals: { trip: trip }
)
end
end

View file

@ -1,13 +0,0 @@
# frozen_string_literal: true
class Trips::CreatePathJob < ApplicationJob
queue_as :default
def perform(trip_id)
trip = Trip.find(trip_id)
trip.calculate_trip_data
trip.save!
end
end

View file

@ -7,7 +7,8 @@ class Trip < ApplicationRecord
validates :name, :started_at, :ended_at, presence: true validates :name, :started_at, :ended_at, presence: true
before_save :calculate_trip_data after_create :enqueue_calculation_jobs
after_update :enqueue_calculation_jobs, if: -> { saved_change_to_started_at? || saved_change_to_ended_at? }
def calculate_trip_data def calculate_trip_data
calculate_path calculate_path
@ -15,6 +16,10 @@ class Trip < ApplicationRecord
calculate_countries calculate_countries
end end
def enqueue_calculation_jobs
Trips::CalculateAllJob.perform_later(id)
end
def points def points
user.tracked_points.where(timestamp: started_at.to_i..ended_at.to_i).order(:timestamp) user.tracked_points.where(timestamp: started_at.to_i..ended_at.to_i).order(:timestamp)
end end
@ -33,21 +38,7 @@ class Trip < ApplicationRecord
@photo_sources ||= photos.map { _1[:source] }.uniq @photo_sources ||= photos.map { _1[:source] }.uniq
end end
private # These methods are now public since they're called from jobs
def photos
@photos ||= Trips::Photos.new(self, user).call
end
def select_dominant_orientation(photos)
vertical_photos = photos.select { |photo| photo[:orientation] == 'portrait' }
horizontal_photos = photos.select { |photo| photo[:orientation] == 'landscape' }
# this is ridiculous, but I couldn't find my way around frontend
# to show all photos in the same height
vertical_photos.count > horizontal_photos.count ? vertical_photos : horizontal_photos
end
def calculate_path def calculate_path
trip_path = Tracks::BuildPath.new(points.pluck(:lonlat)).call trip_path = Tracks::BuildPath.new(points.pluck(:lonlat)).call
@ -65,4 +56,19 @@ class Trip < ApplicationRecord
self.visited_countries = countries self.visited_countries = countries
end end
private
def photos
@photos ||= Trips::Photos.new(self, user).call
end
def select_dominant_orientation(photos)
vertical_photos = photos.select { |photo| photo[:orientation] == 'portrait' }
horizontal_photos = photos.select { |photo| photo[:orientation] == 'landscape' }
# this is ridiculous, but I couldn't find my way around frontend
# to show all photos in the same height
vertical_photos.count > horizontal_photos.count ? vertical_photos : horizontal_photos
end
end end

View file

@ -0,0 +1,14 @@
<% if trip.countries.any? %>
<p class="text-lg text-base-content/60">
<%= "#{trip.countries.join(', ')} (#{trip.distance} #{DISTANCE_UNIT})" %>
</p>
<% elsif trip.visited_countries.present? %>
<p class="text-lg text-base-content/60">
<%= "#{trip.visited_countries.join(', ')} (#{trip.distance} #{DISTANCE_UNIT})" %>
</p>
<% else %>
<p class="text-md text-base-content/60">
<span>Countries are being calculated...</span>
<span class="loading loading-dots loading-sm"></span>
</p>
<% end %>

View file

@ -0,0 +1,6 @@
<% if trip.distance.present? %>
<span><%= trip.distance %> <%= DISTANCE_UNIT %></span>
<% else %>
<span>Calculating...</span>
<span class="loading loading-dots loading-sm"></span>
<% end %>

View file

@ -0,0 +1,24 @@
<% if trip.path.present? %>
<div
id='map'
class="w-full h-full rounded-lg z-0"
data-controller="trips"
data-trips-target="container"
data-distance_unit="<%= DISTANCE_UNIT %>"
data-api_key="<%= current_user.api_key %>"
data-user_settings="<%= current_user.settings.to_json %>"
data-path="<%= trip.path.to_json %>"
data-started_at="<%= trip.started_at %>"
data-ended_at="<%= trip.ended_at %>"
data-timezone="<%= Rails.configuration.time_zone %>">
<div data-trips-target="container" class="h-[25rem] w-full min-h-screen">
</div>
</div>
<% else %>
<div class="flex items-center justify-center h-full">
<div class="text-center">
<p class="text-base-content/60">Trip path is being calculated...</p>
<div class="loading loading-spinner loading-lg mt-4"></div>
</div>
</div>
<% end %>

View file

@ -1,36 +1,31 @@
<% content_for :title, @trip.name %> <% content_for :title, @trip.name %>
<%= turbo_stream_from "trip_#{@trip.id}" %>
<div class="container mx-auto px-4 max-w-4xl my-5"> <div class="container mx-auto px-4 max-w-4xl my-5">
<div class="text-center mb-8"> <div class="text-center mb-8">
<h1 class="text-4xl font-bold mb-2"><%= @trip.name %></h1> <h1 class="text-4xl font-bold mb-2"><%= @trip.name %></h1>
<p class="text-md text-base-content/60"> <p class="text-md text-base-content/60">
<%= human_date(@trip.started_at) %> - <%= human_date(@trip.ended_at) %> <%= human_date(@trip.started_at) %> - <%= human_date(@trip.ended_at) %>
</p> </p>
<% if @trip.countries.any? %> <% if @trip.countries.any? || @trip.visited_countries.present? %>
<p class="text-lg text-base-content/60"> <div id="trip_countries">
<%= "#{@trip.countries.join(', ')} (#{@trip.distance} #{DISTANCE_UNIT})" %> <%= render "trips/countries", trip: @trip %>
</p> </div>
<% else %>
<div id="trip_countries">
<p class="text-lg text-base-content/60">
<span>Countries are being calculated...</span>
<span class="loading loading-dots loading-sm"></span>
</p>
</div>
<% end %> <% end %>
</div> </div>
<div class="bg-base-100 my-8 p-4"> <div class="bg-base-100 my-8 p-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="w-full"> <div class="w-full" id="trip_path">
<div <%= render "trips/path", trip: @trip, current_user: current_user %>
id='map'
class="w-full h-full rounded-lg z-0"
data-controller="trips"
data-trips-target="container"
data-distance_unit="<%= DISTANCE_UNIT %>"
data-api_key="<%= current_user.api_key %>"
data-user_settings="<%= current_user.settings.to_json %>"
data-path="<%= @trip.path.to_json %>"
data-started_at="<%= @trip.started_at %>"
data-ended_at="<%= @trip.ended_at %>"
data-timezone="<%= Rails.configuration.time_zone %>">
<div data-trips-target="container" class="h-[25rem] w-full min-h-screen">
</div>
</div>
</div> </div>
<div class="w-full"> <div class="w-full">
<div> <div>

View file

@ -70,7 +70,7 @@ services:
PROMETHEUS_EXPORTER_HOST: 0.0.0.0 PROMETHEUS_EXPORTER_HOST: 0.0.0.0
PROMETHEUS_EXPORTER_PORT: 9394 PROMETHEUS_EXPORTER_PORT: 9394
SELF_HOSTED: "true" SELF_HOSTED: "true"
STORE_GEODATA: "false" STORE_GEODATA: "true"
logging: logging:
driver: "json-file" driver: "json-file"
options: options:
@ -123,7 +123,7 @@ services:
PROMETHEUS_EXPORTER_HOST: dawarich_app PROMETHEUS_EXPORTER_HOST: dawarich_app
PROMETHEUS_EXPORTER_PORT: 9394 PROMETHEUS_EXPORTER_PORT: 9394
SELF_HOSTED: "true" SELF_HOSTED: "true"
STORE_GEODATA: "false" STORE_GEODATA: "true"
logging: logging:
driver: "json-file" driver: "json-file"
options: options: