mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 01:01:39 -05:00
Add distance calculation and trip cards to trips index
This commit is contained in:
parent
4dca91b62d
commit
782aeb89af
11 changed files with 118 additions and 119 deletions
|
|
@ -1 +1 @@
|
|||
0.17.2
|
||||
0.18.0
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
60
app/javascript/controllers/trip_map_controller.js
Normal file
60
app/javascript/controllers/trip_map_controller.js
Normal 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: "© <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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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
2
db/schema.rb
generated
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue