From 7bd098b54f316cebd9424581b3932c82d6844878 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Thu, 3 Jul 2025 20:34:41 +0200 Subject: [PATCH] Extract tracks calculation to serializer --- app/controllers/map_controller.rb | 29 +------- app/javascript/maps/helpers.js | 12 ++- app/javascript/maps/tracks.js | 2 +- app/serializers/track_serializer.rb | 47 ++++++++++++ spec/serializers/track_serializer_spec.rb | 89 +++++++++++++++++++++++ 5 files changed, 150 insertions(+), 29 deletions(-) create mode 100644 app/serializers/track_serializer.rb create mode 100644 spec/serializers/track_serializer_spec.rb diff --git a/app/controllers/map_controller.rb b/app/controllers/map_controller.rb index c23f3bc2..7147058a 100644 --- a/app/controllers/map_controller.rb +++ b/app/controllers/map_controller.rb @@ -6,31 +6,10 @@ class MapController < ApplicationController def index @points = points.where('timestamp >= ? AND timestamp <= ?', start_at, end_at) - @coordinates = [] - # @points.pluck(:lonlat, :battery, :altitude, :timestamp, :velocity, :id, :country) - # .map { |lonlat, *rest| [lonlat.y, lonlat.x, *rest.map(&:to_s)] } - tracks_data = current_user.tracks - .where('start_at <= ? AND end_at >= ?', Time.zone.at(end_at), Time.zone.at(start_at)) - .order(start_at: :asc) - .pluck(:id, :start_at, :end_at, :distance, :avg_speed, :duration, - :elevation_gain, :elevation_loss, :elevation_max, :elevation_min, :original_path) - - @tracks = tracks_data.map do |id, start_at, end_at, distance, avg_speed, duration, - elevation_gain, elevation_loss, elevation_max, elevation_min, original_path| - { - id: id, - start_at: start_at.iso8601, - end_at: end_at.iso8601, - distance: distance&.to_f || 0, - avg_speed: avg_speed&.to_f || 0, - duration: duration || 0, - elevation_gain: elevation_gain || 0, - elevation_loss: elevation_loss || 0, - elevation_max: elevation_max || 0, - elevation_min: elevation_min || 0, - original_path: original_path&.to_s - } - end + @coordinates = + @points.pluck(:lonlat, :battery, :altitude, :timestamp, :velocity, :id, :country) + .map { |lonlat, *rest| [lonlat.y, lonlat.x, *rest.map(&:to_s)] } + @tracks = TrackSerializer.new(current_user, start_at, end_at).call @distance = distance @start_at = Time.zone.at(start_at) @end_at = Time.zone.at(end_at) diff --git a/app/javascript/maps/helpers.js b/app/javascript/maps/helpers.js index 415f2574..aa5699ab 100644 --- a/app/javascript/maps/helpers.js +++ b/app/javascript/maps/helpers.js @@ -56,13 +56,19 @@ export function minutesToDaysHoursMinutes(minutes) { export function formatDate(timestamp, timezone) { let date; - // Handle both Unix timestamps (numbers) and ISO8601 strings + // Handle different timestamp formats if (typeof timestamp === 'number') { // Unix timestamp in seconds, convert to milliseconds date = new Date(timestamp * 1000); } else if (typeof timestamp === 'string') { - // ISO8601 string, parse directly - date = new Date(timestamp); + // Check if string is a numeric timestamp + if (/^\d+$/.test(timestamp)) { + // String representation of Unix timestamp in seconds + date = new Date(parseInt(timestamp) * 1000); + } else { + // Assume it's an ISO8601 string, parse directly + date = new Date(timestamp); + } } else { // Invalid input return 'Invalid Date'; diff --git a/app/javascript/maps/tracks.js b/app/javascript/maps/tracks.js index b5c0b8b4..1b21069e 100644 --- a/app/javascript/maps/tracks.js +++ b/app/javascript/maps/tracks.js @@ -5,7 +5,7 @@ import { minutesToDaysHoursMinutes } from "../maps/helpers"; // Track-specific color palette - different from regular polylines export const trackColorPalette = { - default: 'blue', // Green - distinct from blue polylines + default: 'red', // Green - distinct from blue polylines hover: '#FF6B35', // Orange-red for hover active: '#E74C3C', // Red for active/clicked start: '#2ECC71', // Green for start marker diff --git a/app/serializers/track_serializer.rb b/app/serializers/track_serializer.rb new file mode 100644 index 00000000..78a4b1ea --- /dev/null +++ b/app/serializers/track_serializer.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +class TrackSerializer + def initialize(user, start_at, end_at) + @user = user + @start_at = start_at + @end_at = end_at + end + + def call + tracks_data = user.tracks + .where('start_at <= ? AND end_at >= ?', Time.zone.at(end_at), Time.zone.at(start_at)) + .order(start_at: :asc) + .pluck(:id, :start_at, :end_at, :distance, :avg_speed, :duration, + :elevation_gain, :elevation_loss, :elevation_max, :elevation_min, :original_path) + + tracks_data.map do |id, start_at, end_at, distance, avg_speed, duration, + elevation_gain, elevation_loss, elevation_max, elevation_min, original_path| + serialize_track_data(id, start_at, end_at, distance, avg_speed, duration, + elevation_gain, elevation_loss, elevation_max, elevation_min, original_path) + end + end + + private + + attr_reader :user, :start_at, :end_at + + def serialize_track_data( + id, start_at, end_at, distance, avg_speed, duration, elevation_gain, + elevation_loss, elevation_max, elevation_min, original_path + ) + + { + id: id, + start_at: start_at.iso8601, + end_at: end_at.iso8601, + distance: distance.to_f, + avg_speed: avg_speed.to_f, + duration: duration, + elevation_gain: elevation_gain, + elevation_loss: elevation_loss, + elevation_max: elevation_max, + elevation_min: elevation_min, + original_path: original_path.to_s + } + end +end diff --git a/spec/serializers/track_serializer_spec.rb b/spec/serializers/track_serializer_spec.rb new file mode 100644 index 00000000..f08641a6 --- /dev/null +++ b/spec/serializers/track_serializer_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe TrackSerializer do + describe '#call' do + let(:user) { create(:user) } + + context 'when serializing user tracks without date range restrictions' do + subject(:serializer) { described_class.new(user, 1.year.ago.to_i, 1.year.from_now.to_i).call } + + let!(:track1) { create(:track, user: user, start_at: 2.hours.ago, end_at: 1.hour.ago) } + let!(:track2) { create(:track, user: user, start_at: 4.hours.ago, end_at: 3.hours.ago) } + + it 'returns an array of serialized tracks' do + expect(serializer).to be_an(Array) + expect(serializer.length).to eq(2) + end + + it 'serializes each track correctly' do + serialized_ids = serializer.map { |track| track[:id] } + expect(serialized_ids).to contain_exactly(track1.id, track2.id) + end + + it 'formats timestamps as ISO8601 for all tracks' do + serializer.each do |track| + expect(track[:start_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/) + expect(track[:end_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/) + end + end + + it 'includes all required fields for each track' do + serializer.each do |track| + expect(track.keys).to contain_exactly( + :id, :start_at, :end_at, :distance, :avg_speed, :duration, + :elevation_gain, :elevation_loss, :elevation_max, :elevation_min, :original_path + ) + end + end + + it 'handles numeric values correctly' do + serializer.each do |track| + expect(track[:distance]).to be_a(Numeric) + expect(track[:avg_speed]).to be_a(Numeric) + expect(track[:duration]).to be_a(Numeric) + expect(track[:elevation_gain]).to be_a(Numeric) + expect(track[:elevation_loss]).to be_a(Numeric) + expect(track[:elevation_max]).to be_a(Numeric) + expect(track[:elevation_min]).to be_a(Numeric) + end + end + end + + context 'when serializing user tracks with date range' do + subject(:serializer) { described_class.new(user, start_at.to_i, end_at.to_i).call } + + let(:start_at) { 6.hours.ago } + let(:end_at) { 30.minutes.ago } + let!(:track_in_range) { create(:track, user: user, start_at: 2.hours.ago, end_at: 1.hour.ago) } + let!(:track_out_of_range) { create(:track, user: user, start_at: 10.hours.ago, end_at: 9.hours.ago) } + + it 'returns an array of serialized tracks' do + expect(serializer).to be_an(Array) + expect(serializer.length).to eq(1) + end + + it 'only includes tracks within the date range' do + serialized_ids = serializer.map { |track| track[:id] } + expect(serialized_ids).to contain_exactly(track_in_range.id) + expect(serialized_ids).not_to include(track_out_of_range.id) + end + + it 'formats timestamps as ISO8601' do + serializer.each do |track| + expect(track[:start_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/) + expect(track[:end_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/) + end + end + end + + context 'when user has no tracks' do + subject(:serializer) { described_class.new(user, 1.day.ago.to_i, Time.current.to_i).call } + + it 'returns an empty array' do + expect(serializer).to eq([]) + end + end + end +end