Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/tracks #724

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .app_version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.23.5
0.23.6
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

# 0.23.6 - 2025-01-23

### Added

- Enabled Postgis extension for PostgreSQL.
- Trips are now store their paths in the database independently of the points.
- Trips are now being rendered on the map using their precalculated paths instead of list of coordinates.

### Changed

- Requesting photos on the Map page now uses the start and end dates from the URL params. #589

# 0.23.5 - 2025-01-22

### Added
Expand Down
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ gem 'lograge'
gem 'oj'
gem 'pg'
gem 'prometheus_exporter'
gem 'activerecord-postgis-adapter', github: 'StoneGod/activerecord-postgis-adapter', branch: 'rails-8'
gem 'puma'
gem 'pundit'
gem 'rails', '~> 8.0'
gem 'rgeo'
gem 'rswag-api'
gem 'rswag-ui'
gem 'shrine', '~> 3.6'
Expand Down
15 changes: 15 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
GIT
remote: https://github.com/StoneGod/activerecord-postgis-adapter.git
revision: 147fd43191ef703e2a1b3654f31d9139201a87e8
branch: rails-8
specs:
activerecord-postgis-adapter (10.0.1)
activerecord (~> 8.0.0)
rgeo-activerecord (~> 8.0.0)

GIT
remote: https://github.com/alexreisner/geocoder.git
revision: 04ee2936a30b30a23ded5231d7faf6cf6c27c099
Expand Down Expand Up @@ -314,6 +323,10 @@ GEM
actionpack (>= 5.2)
railties (>= 5.2)
rexml (3.3.8)
rgeo (3.0.1)
rgeo-activerecord (8.0.0)
activerecord (>= 7.0)
rgeo (>= 3.0)
rspec-core (3.13.2)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.3)
Expand Down Expand Up @@ -443,6 +456,7 @@ PLATFORMS
x86_64-linux

DEPENDENCIES
activerecord-postgis-adapter!
bootsnap
chartkick
data_migrate
Expand Down Expand Up @@ -470,6 +484,7 @@ DEPENDENCIES
pundit
rails (~> 8.0)
redis
rgeo
rspec-rails
rswag-api
rswag-specs
Expand Down
3 changes: 2 additions & 1 deletion app/assets/stylesheets/actiontext.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
background-color: white !important;
}

.trix-content {
.trix-content-editor {
min-height: 10rem;
width: 100%;
}
23 changes: 23 additions & 0 deletions app/controllers/api/v1/tracks_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

class Api::V1::TracksController < ApiController
def index
@tracks =
current_api_user.tracks.where(id: params[:ids]).page(params[:page])
.per(params[:per_page] || 100)

serialized_tracks =
@tracks.map { |track| Api::TrackSerializer.new(track).call }

response.set_header('X-Current-Page', @tracks.current_page.to_s)
response.set_header('X-Total-Pages', @tracks.total_pages.to_s)

render json: serialized_tracks
end

def show
@track = current_api_user.tracks.find(params[:id])

render json: Api::TrackSerializer.new(@track).call
end
end
21 changes: 18 additions & 3 deletions app/controllers/map_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ class MapController < ApplicationController
def index
@points = points.where('timestamp >= ? AND timestamp <= ?', start_at, end_at)

@coordinates =
@points.pluck(:latitude, :longitude, :battery, :altitude, :timestamp, :velocity, :id, :country)
.map { [_1.to_f, _2.to_f, _3.to_s, _4.to_s, _5.to_s, _6.to_s, _7.to_s, _8.to_s] }
@track_ids = track_ids

@coordinates = []
# @coordinates =
# @points.pluck(:latitude, :longitude, :battery, :altitude, :timestamp, :velocity, :id, :country)
# .map { [_1.to_f, _2.to_f, _3.to_s, _4.to_s, _5.to_s, _6.to_s, _7.to_s, _8.to_s] }
@distance = distance
@start_at = Time.zone.at(start_at)
@end_at = Time.zone.at(end_at)
Expand Down Expand Up @@ -53,4 +56,16 @@ def points_from_import
def points_from_user
current_user.tracked_points.without_raw_data.order(timestamp: :asc)
end

def track_ids
started_at = params[:start_at].present? ? Time.zone.parse(params[:start_at]) : Time.zone.today.beginning_of_day
ended_at = params[:end_at].present? ? Time.zone.parse(params[:end_at]) : Time.zone.today.end_of_day

current_user
.tracks
.where(started_at: started_at..ended_at)
.or(current_user.tracks.where(ended_at: started_at..ended_at))
.order(started_at: :asc)
.pluck(:id)
end
end
5 changes: 0 additions & 5 deletions app/controllers/trips_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@ def index
end

def show
@coordinates = @trip.points.pluck(
:latitude, :longitude, :battery, :altitude, :timestamp, :velocity, :id,
:country
).map { [_1.to_f, _2.to_f, _3.to_s, _4.to_s, _5.to_s, _6.to_s, _7.to_s, _8.to_s] }

@photo_previews = Rails.cache.fetch("trip_photos_#{@trip.id}", expires_in: 1.day) do
@trip.photo_previews
end
Expand Down
2 changes: 2 additions & 0 deletions app/helpers/api/v1/tracks_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module Api::V1::TracksHelper
end
4 changes: 4 additions & 0 deletions app/javascript/controllers/datetime_controller.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// This controller is being used on:
// - trips/new
// - trips/edit

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
Expand Down
136 changes: 125 additions & 11 deletions app/javascript/controllers/maps_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
updatePolylinesOpacity,
updatePolylinesColors,
calculateSpeed,
getSpeedColor
getSpeedColor,
createTrackPolyline
} from "../maps/polylines";

import { fetchAndDrawAreas } from "../maps/areas";
Expand Down Expand Up @@ -56,6 +57,7 @@ export default class extends Controller {
this.liveMapEnabled = this.userSettings.live_map_enabled || false;
this.countryCodesMap = countryCodesMap();
this.speedColoredPolylines = this.userSettings.speed_colored_routes || false;
this.fetchAndRenderTracks();

this.center = this.markers[this.markers.length - 1] || [52.514568, 13.350111];

Expand All @@ -76,14 +78,8 @@ export default class extends Controller {
},
onAdd: (map) => {
const div = L.DomUtil.create('div', 'leaflet-control-stats');
const distance = this.element.dataset.distance || '0';
const pointsNumber = this.element.dataset.points_number || '0';
const unit = this.distanceUnit === 'mi' ? 'mi' : 'km';
div.innerHTML = `${distance} ${unit} | ${pointsNumber} points`;
div.style.backgroundColor = 'white';
div.style.padding = '0 5px';
div.style.marginRight = '5px';
div.style.display = 'inline-block';
this.statsDiv = div; // Store reference to the div
this.updateStats(); // Initial update
return div;
}
});
Expand All @@ -102,6 +98,7 @@ export default class extends Controller {
this.heatmapMarkers = this.markersArray.map((element) => [element._latlng.lat, element._latlng.lng, 0.2]);

this.polylinesLayer = createPolylinesLayer(this.markers, this.map, this.timezone, this.routeOpacity, this.userSettings, this.distanceUnit);
this.updateStats();
this.heatmapLayer = L.heatLayer(this.heatmapMarkers, { radius: 20 }).addTo(this.map);

// Create a proper Leaflet layer for fog
Expand Down Expand Up @@ -218,8 +215,8 @@ export default class extends Controller {
}

const urlParams = new URLSearchParams(window.location.search);
const startDate = urlParams.get('start_at')?.split('T')[0] || new Date().toISOString().split('T')[0];
const endDate = urlParams.get('end_at')?.split('T')[0] || new Date().toISOString().split('T')[0];
const startDate = urlParams.get('start_at') || new Date().toISOString();
const endDate = urlParams.get('end_at')|| new Date().toISOString();
await fetchAndDisplayPhotos({
map: this.map,
photoMarkers: this.photoMarkers,
Expand All @@ -240,6 +237,11 @@ export default class extends Controller {
if (this.liveMapEnabled) {
this.setupSubscription();
}

// After map is initialized, fetch and render tracks
this.map.whenReady(() => {
this.fetchAndRenderTracks();
});
}

disconnect() {
Expand Down Expand Up @@ -295,6 +297,7 @@ export default class extends Controller {
this.userSettings,
this.distanceUnit
);
this.updateStats();

// Pan map to new location
this.map.setView([newPoint[0], newPoint[1]], 16);
Expand Down Expand Up @@ -475,6 +478,7 @@ export default class extends Controller {
this.userSettings,
this.distanceUnit
);
this.updateStats();
if (wasPolyLayerVisible) {
// Add new polylines layer to map and to layer control
this.polylinesLayer.addTo(this.map);
Expand Down Expand Up @@ -802,13 +806,15 @@ export default class extends Controller {
this.polylinesLayer,
newSettings.speed_colored_routes
);
this.updateStats();
}
}

if (newSettings.route_opacity !== this.userSettings.route_opacity) {
const newOpacity = parseFloat(newSettings.route_opacity) || 0.6;
if (this.polylinesLayer) {
updatePolylinesOpacity(this.polylinesLayer, newOpacity);
this.updateStats();
}
}

Expand Down Expand Up @@ -1291,5 +1297,113 @@ export default class extends Controller {
}
return `${hours}h`;
}

updateStats() {
if (!this.statsDiv) return;

const distance = this.element.dataset.distance || '0';
const pointsNumber = this.element.dataset.points_number || '0';
const unit = this.distanceUnit === 'mi' ? 'mi' : 'km';
const polylinesCount = this.polylinesLayer ? this.polylinesLayer.getLayers().length : 0;

this.statsDiv.innerHTML = `${distance} ${unit} | ${pointsNumber} points | ${polylinesCount} routes`;
this.statsDiv.style.backgroundColor = 'white';
this.statsDiv.style.padding = '0 5px';
this.statsDiv.style.marginRight = '5px';
this.statsDiv.style.display = 'inline-block';
}

async fetchAndRenderTracks() {
const trackIdsString = this.element.dataset.track_ids;
console.log('Track IDs string:', trackIdsString);
if (!trackIdsString || !this.map) {
console.log('Early return - missing data:', { trackIdsString: !!trackIdsString, map: !!this.map });
return;
}

const trackIds = trackIdsString.replace(/[\[\]]/g, '').split(',').map(id => id.trim());
console.log(`Total tracks to fetch: ${trackIds.length}`);

try {
// Create the layer group and store it as a class property
if (!this.tracksLayer) {
console.log('Creating new tracks layer');
this.tracksLayer = L.layerGroup();
if (this.map) {
console.log('Adding tracks layer to map');
this.tracksLayer.addTo(this.map);

if (this.layerControl) {
this.layerControl.addOverlay(this.tracksLayer, 'Tracks');
}
}
}

// Create shared renderer
const renderer = L.canvas({ padding: 0.5, pane: 'overlayPane' });

// Process tracks in smaller chunks to avoid URL length limits
const CHUNK_SIZE = 50; // Number of track IDs to include in each request
const BATCH_SIZE = 10; // Number of tracks to process at once from the response

for (let i = 0; i < trackIds.length; i += CHUNK_SIZE) {
const chunk = trackIds.slice(i, i + CHUNK_SIZE);
console.log(`Fetching chunk ${Math.floor(i/CHUNK_SIZE) + 1}/${Math.ceil(trackIds.length/CHUNK_SIZE)}`);

try {
const response = await fetch(
`/api/v1/tracks?ids=${chunk.join(',')}&per_page=${CHUNK_SIZE}&api_key=${this.apiKey}`,
{
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
credentials: 'same-origin'
}
);

if (!response.ok) {
console.warn(`Failed to fetch chunk: ${response.status}`);
continue;
}

const tracks = await response.json();
console.log(`Rendering ${tracks.length} tracks from chunk`);

// Process tracks in smaller batches
for (let j = 0; j < tracks.length; j += BATCH_SIZE) {
const batch = tracks.slice(j, j + BATCH_SIZE);

batch.forEach(track => {
try {
const trackLayer = createTrackPolyline(track, this.map, this.userSettings, renderer);
if (trackLayer && this.tracksLayer) {
this.tracksLayer.addLayer(trackLayer);
}
} catch (error) {
console.warn(`Failed to render track ${track.id}:`, error);
}
});

// Small delay between batches to allow browser to breathe
if (j + BATCH_SIZE < tracks.length) {
await new Promise(resolve => setTimeout(resolve, 10));
}
}

} catch (error) {
console.error(`Error processing chunk:`, error);
}

// Small delay between chunks
if (i + CHUNK_SIZE < trackIds.length) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}

} catch (error) {
console.error('Error in track fetching/rendering:', error);
}
}
}

Loading