Add distance calculation and trip cards to trips index

This commit is contained in:
Eugene Burmakin 2024-11-28 15:29:17 +01:00
parent 4dca91b62d
commit 782aeb89af
11 changed files with 118 additions and 119 deletions

View file

@ -1 +1 @@
0.17.2
0.18.0

View file

@ -6,7 +6,7 @@ class TripsController < ApplicationController
before_action :set_coordinates, only: %i[show edit]
def index
@trips = current_user.trips.order(created_at: :desc).page(params[:page])
@trips = current_user.trips.order(started_at: :desc).page(params[:page]).per(6)
end
def show
@ -22,6 +22,7 @@ class TripsController < ApplicationController
def new
@trip = Trip.new
@coordinates = []
end
def edit; end
@ -55,7 +56,6 @@ class TripsController < ApplicationController
@trip = current_user.trips.find(params[:id])
end
def set_coordinates
@coordinates = @trip.points.pluck(
:latitude, :longitude, :battery, :altitude, :timestamp, :velocity, :id,
@ -64,6 +64,6 @@ class TripsController < ApplicationController
end
def trip_params
params.require(:trip).permit(:name, :started_at, :ended_at, :notes, :field_notes)
params.require(:trip).permit(:name, :started_at, :ended_at, :notes)
end
end

View file

@ -788,88 +788,6 @@ export default class extends Controller {
this.layerControl = L.control.layers(this.baseMaps(), layerControl).addTo(this.map);
}
// async fetchAndDisplayPhotos(startDate, endDate, retryCount = 0) {
// const MAX_RETRIES = 3;
// const RETRY_DELAY = 3000; // 3 seconds
// // Create loading control
// const LoadingControl = L.Control.extend({
// onAdd: (map) => {
// const container = L.DomUtil.create('div', 'leaflet-loading-control');
// container.innerHTML = '<div class="loading-spinner"></div>';
// return container;
// }
// });
// const loadingControl = new LoadingControl({ position: 'topleft' });
// this.map.addControl(loadingControl);
// try {
// const params = new URLSearchParams({
// api_key: this.apiKey,
// start_date: startDate,
// end_date: endDate
// });
// const response = await fetch(`/api/v1/photos?${params}`);
// if (!response.ok) {
// throw new Error(`HTTP error! status: ${response.status}`);
// }
// const photos = await response.json();
// this.photoMarkers.clearLayers();
// // Create a promise for each photo to track when it's fully loaded
// const photoLoadPromises = photos.map(photo => {
// return new Promise((resolve) => {
// const img = new Image();
// const thumbnailUrl = `/api/v1/photos/${photo.id}/thumbnail.jpg?api_key=${this.apiKey}`;
// img.onload = () => {
// this.createPhotoMarker(photo);
// resolve();
// };
// img.onerror = () => {
// console.error(`Failed to load photo ${photo.id}`);
// resolve(); // Resolve anyway to not block other photos
// };
// img.src = thumbnailUrl;
// });
// });
// // Wait for all photos to be loaded and rendered
// await Promise.all(photoLoadPromises);
// if (!this.map.hasLayer(this.photoMarkers)) {
// this.photoMarkers.addTo(this.map);
// }
// // Show checkmark for 1 second before removing
// const loadingSpinner = document.querySelector('.loading-spinner');
// loadingSpinner.classList.add('done');
// await new Promise(resolve => setTimeout(resolve, 1000));
// } catch (error) {
// console.error('Error fetching photos:', error);
// showFlashMessage('error', 'Failed to fetch photos');
// if (retryCount < MAX_RETRIES) {
// console.log(`Retrying in ${RETRY_DELAY/1000} seconds... (Attempt ${retryCount + 1}/${MAX_RETRIES})`);
// setTimeout(() => {
// this.fetchAndDisplayPhotos(startDate, endDate, retryCount + 1);
// }, RETRY_DELAY);
// } else {
// showFlashMessage('error', 'Failed to fetch photos after multiple attempts');
// }
// } finally {
// // Remove loading control after the delay
// this.map.removeControl(loadingControl);
// }
// }
createPhotoMarker(photo) {
if (!photo.exifInfo?.latitude || !photo.exifInfo?.longitude) return;

View file

@ -0,0 +1,60 @@
import { Controller } from "@hotwired/stimulus"
import L from "leaflet"
export default class extends Controller {
static values = {
tripId: Number,
coordinates: Array,
apiKey: String,
userSettings: Object,
timezone: String,
distanceUnit: String
}
connect() {
setTimeout(() => {
this.initializeMap()
}, 100)
}
initializeMap() {
// Initialize map with basic configuration
this.map = L.map(this.element, {
zoomControl: false,
dragging: false,
scrollWheelZoom: false,
attributionControl: true // Disable default attribution control
})
// Add the tile layer
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: "&copy; <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a>"
}).addTo(this.map)
// If we have coordinates, show the route
if (this.hasCoordinatesValue && this.coordinatesValue.length > 0) {
this.showRoute()
}
}
showRoute() {
const points = this.coordinatesValue.map(coord => [coord[0], coord[1]])
const polyline = L.polyline(points, {
color: 'blue',
weight: 3,
opacity: 0.8
}).addTo(this.map)
this.map.fitBounds(polyline.getBounds(), {
padding: [20, 20]
})
}
disconnect() {
if (this.map) {
this.map.remove()
}
}
}

View file

@ -142,7 +142,7 @@ export default class extends Controller {
const polyline = L.polyline(points, {
color: 'blue',
weight: 3,
opacity: 0.6
opacity: 0.8
})
// Add to polylines layer instead of directly to map
this.polylinesLayer.addTo(this.map)

View file

@ -1,14 +1,16 @@
# frozen_string_literal: true
class Trip < ApplicationRecord
has_rich_text :field_notes
has_rich_text :notes
belongs_to :user
validates :name, :started_at, :ended_at, presence: true
before_save :calculate_distance
def points
user.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
def countries
@ -34,4 +36,20 @@ class Trip < ApplicationRecord
{ url: "/api/v1/photos/#{asset['id']}/thumbnail.jpg?api_key=#{user.api_key}" }
end
end
private
def calculate_distance
distance = 0
points.each_cons(2) do |point1, point2|
distance_between = Geocoder::Calculations.distance_between(
point1.to_coordinates, point2.to_coordinates, units: ::DISTANCE_UNIT
)
distance += distance_between
end
self.distance = distance.round
end
end

View file

@ -20,7 +20,7 @@
data-distance_unit="<%= DISTANCE_UNIT %>"
data-api_key="<%= current_user.api_key %>"
data-user_settings="<%= current_user.settings.to_json %>"
data-coordinates="<%= [].to_json %>"
data-coordinates="<%= @coordinates.to_json %>"
data-timezone="<%= Rails.configuration.time_zone %>">
</div>
</div>
@ -61,8 +61,8 @@
</div>
<div class="form-control">
<%= form.label :field_notes %>
<%= form.rich_text_area :field_notes %>
<%= form.label :notes %>
<%= form.rich_text_area :notes %>
</div>
<div>

View file

@ -24,29 +24,32 @@
<%= paginate @trips %>
</div>
</div>
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Started at</th>
<th>Ended at</th>
</tr>
</thead>
<tbody
data-controller="trips"
data-trips-target="index"
data-user-id="<%= current_user.id %>"
>
<% @trips.each do |trip| %>
<tr data-trip-id="<%= trip.id %>" id="trip-<%= trip.id %>">
<td><%= link_to trip.name, trip, class: 'underline hover:no-underline' %></td>
<td><%= trip.started_at.strftime("%d.%m.%Y, %H:%M") %></td>
<td><%= trip.ended_at.strftime("%d.%m.%Y, %H:%M") %></td>
</tr>
<% end %>
</tbody>
</table>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 my-4">
<% @trips.each do |trip| %>
<div class="card bg-base-200 shadow-xl" data-trip-id="<%= trip.id %>" id="trip-<%= trip.id %>">
<div class="card-body">
<h2 class="card-title justify-center">
<%= link_to trip.name, trip, class: 'hover:underline' %>
</h2>
<p class="text-sm text-gray-600 text-center">
<%= "#{human_date(trip.started_at)} #{human_date(trip.ended_at)}, #{trip.distance} #{DISTANCE_UNIT}" %>
</p>
<div style="width: 100%; aspect-ratio: 1/1;"
id="map-<%= trip.id %>"
class="rounded-lg"
data-controller="trip-map"
data-trip-map-trip-id-value="<%= trip.id %>"
data-trip-map-coordinates-value="<%= trip.points.pluck(:latitude, :longitude, :battery, :altitude, :timestamp, :velocity, :id, :country).to_json %>"
data-trip-map-api-key-value="<%= current_user.api_key %>"
data-trip-map-user-settings-value="<%= current_user.settings.to_json %>"
data-trip-map-timezone-value="<%= Rails.configuration.time_zone %>"
data-trip-map-distance-unit-value="<%= DISTANCE_UNIT %>">
</div>
</div>
</div>
<% end %>
</div>
<% end %>
</div>

View file

@ -9,7 +9,7 @@
</p>
<% if @trip.countries.any? %>
<p class="text-lg text-base-content/60">
<%= @trip.countries.join(', ') %>
<%= "#{@trip.countries.join(', ')} (#{@trip.distance} #{DISTANCE_UNIT})" %>
</p>
<% end %>
</div>
@ -34,7 +34,7 @@
</div>
<div class="w-full">
<div>
<%= @trip.field_notes.body %>
<%= @trip.notes.body %>
</div>
<!-- Photos Grid Section -->

View file

@ -6,7 +6,7 @@ class CreateTrips < ActiveRecord::Migration[7.2]
t.string :name, null: false
t.datetime :started_at, null: false
t.datetime :ended_at, null: false
t.text :notes
t.integer :distance
t.references :user, null: false, foreign_key: true
t.timestamps

2
db/schema.rb generated
View file

@ -189,7 +189,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_11_28_095325) do
t.string "name", null: false
t.datetime "started_at", null: false
t.datetime "ended_at", null: false
t.text "notes"
t.integer "distance"
t.bigint "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false