mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Add tracks to map
This commit is contained in:
parent
7bd098b54f
commit
565f92c463
12 changed files with 618 additions and 96 deletions
|
|
@ -7,9 +7,9 @@ class MapController < ApplicationController
|
||||||
@points = points.where('timestamp >= ? AND timestamp <= ?', start_at, end_at)
|
@points = points.where('timestamp >= ? AND timestamp <= ?', start_at, end_at)
|
||||||
|
|
||||||
@coordinates =
|
@coordinates =
|
||||||
@points.pluck(:lonlat, :battery, :altitude, :timestamp, :velocity, :id, :country)
|
@points.pluck(:lonlat, :battery, :altitude, :timestamp, :velocity, :id, :country, :track_id)
|
||||||
.map { |lonlat, *rest| [lonlat.y, lonlat.x, *rest.map(&:to_s)] }
|
.map { |lonlat, *rest| [lonlat.y, lonlat.x, *rest.map(&:to_s)] }
|
||||||
@tracks = TrackSerializer.new(current_user, start_at, end_at).call
|
@tracks = TrackSerializer.new(current_user, @coordinates).call
|
||||||
@distance = distance
|
@distance = distance
|
||||||
@start_at = Time.zone.at(start_at)
|
@start_at = Time.zone.at(start_at)
|
||||||
@end_at = Time.zone.at(end_at)
|
@end_at = Time.zone.at(end_at)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ import {
|
||||||
updatePolylinesColors,
|
updatePolylinesColors,
|
||||||
colorFormatEncode,
|
colorFormatEncode,
|
||||||
colorFormatDecode,
|
colorFormatDecode,
|
||||||
colorStopsFallback
|
colorStopsFallback,
|
||||||
|
reestablishPolylineEventHandlers,
|
||||||
|
managePaneVisibility
|
||||||
} from "../maps/polylines";
|
} from "../maps/polylines";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -205,6 +207,9 @@ export default class extends BaseController {
|
||||||
// Add the toggle panel button
|
// Add the toggle panel button
|
||||||
this.addTogglePanelButton();
|
this.addTogglePanelButton();
|
||||||
|
|
||||||
|
// Add routes/tracks selector
|
||||||
|
this.addRoutesTracksSelector();
|
||||||
|
|
||||||
// Check if we should open the panel based on localStorage or URL params
|
// Check if we should open the panel based on localStorage or URL params
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const isPanelOpen = localStorage.getItem('mapPanelOpen') === 'true';
|
const isPanelOpen = localStorage.getItem('mapPanelOpen') === 'true';
|
||||||
|
|
@ -553,6 +558,33 @@ export default class extends BaseController {
|
||||||
const selectedLayerName = event.name;
|
const selectedLayerName = event.name;
|
||||||
this.updatePreferredBaseLayer(selectedLayerName);
|
this.updatePreferredBaseLayer(selectedLayerName);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add event listeners for overlay layer changes to keep routes/tracks selector in sync
|
||||||
|
this.map.on('overlayadd', (event) => {
|
||||||
|
if (event.name === 'Routes') {
|
||||||
|
this.handleRouteLayerToggle('routes');
|
||||||
|
// Re-establish event handlers when routes are manually added
|
||||||
|
if (event.layer === this.polylinesLayer) {
|
||||||
|
reestablishPolylineEventHandlers(this.polylinesLayer, this.map, this.userSettings, this.distanceUnit);
|
||||||
|
}
|
||||||
|
} else if (event.name === 'Tracks') {
|
||||||
|
this.handleRouteLayerToggle('tracks');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manage pane visibility when layers are manually toggled
|
||||||
|
this.updatePaneVisibilityAfterLayerChange();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.map.on('overlayremove', (event) => {
|
||||||
|
if (event.name === 'Routes' || event.name === 'Tracks') {
|
||||||
|
// Don't auto-switch when layers are manually turned off
|
||||||
|
// Just update the radio button state to reflect current visibility
|
||||||
|
this.updateRadioButtonState();
|
||||||
|
|
||||||
|
// Manage pane visibility when layers are manually toggled
|
||||||
|
this.updatePaneVisibilityAfterLayerChange();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePreferredBaseLayer(selectedLayerName) {
|
updatePreferredBaseLayer(selectedLayerName) {
|
||||||
|
|
@ -1056,11 +1088,27 @@ export default class extends BaseController {
|
||||||
const layer = controlsLayer[name];
|
const layer = controlsLayer[name];
|
||||||
if (wasVisible && layer) {
|
if (wasVisible && layer) {
|
||||||
layer.addTo(this.map);
|
layer.addTo(this.map);
|
||||||
|
// Re-establish event handlers for polylines layer when it's re-added
|
||||||
|
if (name === 'Routes' && layer === this.polylinesLayer) {
|
||||||
|
reestablishPolylineEventHandlers(this.polylinesLayer, this.map, this.userSettings, this.distanceUnit);
|
||||||
|
}
|
||||||
} else if (layer && this.map.hasLayer(layer)) {
|
} else if (layer && this.map.hasLayer(layer)) {
|
||||||
this.map.removeLayer(layer);
|
this.map.removeLayer(layer);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Manage pane visibility based on which layers are visible
|
||||||
|
const routesVisible = this.map.hasLayer(this.polylinesLayer);
|
||||||
|
const tracksVisible = this.tracksLayer && this.map.hasLayer(this.tracksLayer);
|
||||||
|
|
||||||
|
if (routesVisible && !tracksVisible) {
|
||||||
|
managePaneVisibility(this.map, 'routes');
|
||||||
|
} else if (tracksVisible && !routesVisible) {
|
||||||
|
managePaneVisibility(this.map, 'tracks');
|
||||||
|
} else {
|
||||||
|
managePaneVisibility(this.map, 'both');
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating map settings:', error);
|
console.error('Error updating map settings:', error);
|
||||||
console.error(error.stack);
|
console.error(error.stack);
|
||||||
|
|
@ -1154,6 +1202,166 @@ export default class extends BaseController {
|
||||||
this.map.addControl(new TogglePanelControl({ position: 'topright' }));
|
this.map.addControl(new TogglePanelControl({ position: 'topright' }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addRoutesTracksSelector() {
|
||||||
|
// Store reference to the controller instance for use in the control
|
||||||
|
const controller = this;
|
||||||
|
|
||||||
|
const RouteTracksControl = L.Control.extend({
|
||||||
|
onAdd: function(map) {
|
||||||
|
const container = L.DomUtil.create('div', 'routes-tracks-selector leaflet-bar');
|
||||||
|
container.style.backgroundColor = 'white';
|
||||||
|
container.style.padding = '8px';
|
||||||
|
container.style.borderRadius = '4px';
|
||||||
|
container.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)';
|
||||||
|
container.style.fontSize = '12px';
|
||||||
|
container.style.lineHeight = '1.2';
|
||||||
|
|
||||||
|
// Get saved preference or default to 'routes'
|
||||||
|
const savedPreference = localStorage.getItem('mapRouteMode') || 'routes';
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div style="margin-bottom: 4px; font-weight: bold; text-align: center;">Display</div>
|
||||||
|
<div>
|
||||||
|
<label style="display: block; margin-bottom: 4px; cursor: pointer;">
|
||||||
|
<input type="radio" name="route-mode" value="routes" ${savedPreference === 'routes' ? 'checked' : ''} style="margin-right: 4px;">
|
||||||
|
Routes
|
||||||
|
</label>
|
||||||
|
<label style="display: block; cursor: pointer;">
|
||||||
|
<input type="radio" name="route-mode" value="tracks" ${savedPreference === 'tracks' ? 'checked' : ''} style="margin-right: 4px;">
|
||||||
|
Tracks
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Disable map interactions when clicking the control
|
||||||
|
L.DomEvent.disableClickPropagation(container);
|
||||||
|
|
||||||
|
// Add change event listeners
|
||||||
|
const radioButtons = container.querySelectorAll('input[name="route-mode"]');
|
||||||
|
radioButtons.forEach(radio => {
|
||||||
|
L.DomEvent.on(radio, 'change', () => {
|
||||||
|
if (radio.checked) {
|
||||||
|
controller.switchRouteMode(radio.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the control to the map
|
||||||
|
this.map.addControl(new RouteTracksControl({ position: 'topleft' }));
|
||||||
|
|
||||||
|
// Apply initial state based on saved preference
|
||||||
|
const savedPreference = localStorage.getItem('mapRouteMode') || 'routes';
|
||||||
|
this.switchRouteMode(savedPreference, true);
|
||||||
|
|
||||||
|
// Set initial pane visibility
|
||||||
|
this.updatePaneVisibilityAfterLayerChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
switchRouteMode(mode, isInitial = false) {
|
||||||
|
// Save preference to localStorage
|
||||||
|
localStorage.setItem('mapRouteMode', mode);
|
||||||
|
|
||||||
|
if (mode === 'routes') {
|
||||||
|
// Hide tracks layer if it exists and is visible
|
||||||
|
if (this.tracksLayer && this.map.hasLayer(this.tracksLayer)) {
|
||||||
|
this.map.removeLayer(this.tracksLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show routes layer if it exists and is not visible
|
||||||
|
if (this.polylinesLayer && !this.map.hasLayer(this.polylinesLayer)) {
|
||||||
|
this.map.addLayer(this.polylinesLayer);
|
||||||
|
// Re-establish event handlers after adding the layer back
|
||||||
|
reestablishPolylineEventHandlers(this.polylinesLayer, this.map, this.userSettings, this.distanceUnit);
|
||||||
|
} else if (this.polylinesLayer) {
|
||||||
|
reestablishPolylineEventHandlers(this.polylinesLayer, this.map, this.userSettings, this.distanceUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manage pane visibility to fix z-index blocking
|
||||||
|
managePaneVisibility(this.map, 'routes');
|
||||||
|
|
||||||
|
// Update layer control checkboxes
|
||||||
|
this.updateLayerControlCheckboxes('Routes', true);
|
||||||
|
this.updateLayerControlCheckboxes('Tracks', false);
|
||||||
|
} else if (mode === 'tracks') {
|
||||||
|
// Hide routes layer if it exists and is visible
|
||||||
|
if (this.polylinesLayer && this.map.hasLayer(this.polylinesLayer)) {
|
||||||
|
this.map.removeLayer(this.polylinesLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show tracks layer if it exists and is not visible
|
||||||
|
if (this.tracksLayer && !this.map.hasLayer(this.tracksLayer)) {
|
||||||
|
this.map.addLayer(this.tracksLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manage pane visibility to fix z-index blocking
|
||||||
|
managePaneVisibility(this.map, 'tracks');
|
||||||
|
|
||||||
|
// Update layer control checkboxes
|
||||||
|
this.updateLayerControlCheckboxes('Routes', false);
|
||||||
|
this.updateLayerControlCheckboxes('Tracks', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLayerControlCheckboxes(layerName, isVisible) {
|
||||||
|
// Find the layer control input for the specified layer
|
||||||
|
const layerControlContainer = document.querySelector('.leaflet-control-layers');
|
||||||
|
if (!layerControlContainer) return;
|
||||||
|
|
||||||
|
const inputs = layerControlContainer.querySelectorAll('input[type="checkbox"]');
|
||||||
|
inputs.forEach(input => {
|
||||||
|
const label = input.nextElementSibling;
|
||||||
|
if (label && label.textContent.trim() === layerName) {
|
||||||
|
input.checked = isVisible;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRouteLayerToggle(mode) {
|
||||||
|
// Update the radio button selection
|
||||||
|
const radioButtons = document.querySelectorAll('input[name="route-mode"]');
|
||||||
|
radioButtons.forEach(radio => {
|
||||||
|
if (radio.value === mode) {
|
||||||
|
radio.checked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Switch to the selected mode and enforce mutual exclusivity
|
||||||
|
this.switchRouteMode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRadioButtonState() {
|
||||||
|
// Update radio buttons to reflect current layer visibility
|
||||||
|
const routesVisible = this.polylinesLayer && this.map.hasLayer(this.polylinesLayer);
|
||||||
|
const tracksVisible = this.tracksLayer && this.map.hasLayer(this.tracksLayer);
|
||||||
|
|
||||||
|
const radioButtons = document.querySelectorAll('input[name="route-mode"]');
|
||||||
|
radioButtons.forEach(radio => {
|
||||||
|
if (radio.value === 'routes' && routesVisible && !tracksVisible) {
|
||||||
|
radio.checked = true;
|
||||||
|
} else if (radio.value === 'tracks' && tracksVisible && !routesVisible) {
|
||||||
|
radio.checked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePaneVisibilityAfterLayerChange() {
|
||||||
|
// Update pane visibility based on current layer visibility
|
||||||
|
const routesVisible = this.polylinesLayer && this.map.hasLayer(this.polylinesLayer);
|
||||||
|
const tracksVisible = this.tracksLayer && this.map.hasLayer(this.tracksLayer);
|
||||||
|
|
||||||
|
if (routesVisible && !tracksVisible) {
|
||||||
|
managePaneVisibility(this.map, 'routes');
|
||||||
|
} else if (tracksVisible && !routesVisible) {
|
||||||
|
managePaneVisibility(this.map, 'tracks');
|
||||||
|
} else {
|
||||||
|
managePaneVisibility(this.map, 'both');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
toggleRightPanel() {
|
toggleRightPanel() {
|
||||||
if (this.rightPanel) {
|
if (this.rightPanel) {
|
||||||
const panel = document.querySelector('.leaflet-right-panel');
|
const panel = document.querySelector('.leaflet-right-panel');
|
||||||
|
|
@ -1632,21 +1840,12 @@ export default class extends BaseController {
|
||||||
|
|
||||||
// Track-related methods
|
// Track-related methods
|
||||||
async initializeTracksLayer() {
|
async initializeTracksLayer() {
|
||||||
console.log('DEBUG: Initializing tracks layer');
|
|
||||||
console.log('DEBUG: this.tracksData:', this.tracksData);
|
|
||||||
console.log('DEBUG: tracksData type:', typeof this.tracksData);
|
|
||||||
console.log('DEBUG: tracksData length:', this.tracksData ? this.tracksData.length : 'undefined');
|
|
||||||
|
|
||||||
// Use pre-loaded tracks data if available, otherwise fetch from API
|
// Use pre-loaded tracks data if available, otherwise fetch from API
|
||||||
if (this.tracksData && this.tracksData.length > 0) {
|
if (this.tracksData && this.tracksData.length > 0) {
|
||||||
console.log('DEBUG: Using pre-loaded tracks data');
|
|
||||||
this.createTracksFromData(this.tracksData);
|
this.createTracksFromData(this.tracksData);
|
||||||
} else {
|
} else {
|
||||||
console.log('DEBUG: No pre-loaded tracks data, fetching from API');
|
|
||||||
await this.fetchTracks();
|
await this.fetchTracks();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('DEBUG: Tracks layer after initialization:', this.tracksLayer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchTracks() {
|
async fetchTracks() {
|
||||||
|
|
@ -1683,14 +1882,7 @@ export default class extends BaseController {
|
||||||
// Clear existing tracks
|
// Clear existing tracks
|
||||||
this.tracksLayer.clearLayers();
|
this.tracksLayer.clearLayers();
|
||||||
|
|
||||||
console.log('DEBUG: Creating tracks from data:', {
|
|
||||||
tracksData: tracksData,
|
|
||||||
tracksCount: tracksData ? tracksData.length : 0,
|
|
||||||
firstTrack: tracksData && tracksData.length > 0 ? tracksData[0] : null
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!tracksData || tracksData.length === 0) {
|
if (!tracksData || tracksData.length === 0) {
|
||||||
console.log('DEBUG: No tracks data available');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1702,14 +1894,10 @@ export default class extends BaseController {
|
||||||
this.distanceUnit
|
this.distanceUnit
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('DEBUG: Created tracks layer:', newTracksLayer);
|
|
||||||
|
|
||||||
// Add all tracks to the existing tracks layer
|
// Add all tracks to the existing tracks layer
|
||||||
newTracksLayer.eachLayer((layer) => {
|
newTracksLayer.eachLayer((layer) => {
|
||||||
this.tracksLayer.addLayer(layer);
|
this.tracksLayer.addLayer(layer);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('DEBUG: Final tracks layer with', Object.keys(this.tracksLayer._layers).length, 'layers');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLayerControl() {
|
updateLayerControl() {
|
||||||
|
|
|
||||||
|
|
@ -464,6 +464,9 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS
|
||||||
segmentGroup.options.interactive = true;
|
segmentGroup.options.interactive = true;
|
||||||
segmentGroup.options.bubblingMouseEvents = false;
|
segmentGroup.options.bubblingMouseEvents = false;
|
||||||
|
|
||||||
|
// Store the original coordinates for later use
|
||||||
|
segmentGroup._polylineCoordinates = polylineCoordinates;
|
||||||
|
|
||||||
// Add the hover functionality to the group
|
// Add the hover functionality to the group
|
||||||
addHighlightOnHover(segmentGroup, map, polylineCoordinates, userSettings, distanceUnit);
|
addHighlightOnHover(segmentGroup, map, polylineCoordinates, userSettings, distanceUnit);
|
||||||
|
|
||||||
|
|
@ -550,3 +553,120 @@ export function updatePolylinesOpacity(polylinesLayer, opacity) {
|
||||||
segment.setStyle({ opacity: opacity });
|
segment.setStyle({ opacity: opacity });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function reestablishPolylineEventHandlers(polylinesLayer, map, userSettings, distanceUnit) {
|
||||||
|
let groupsProcessed = 0;
|
||||||
|
let segmentsProcessed = 0;
|
||||||
|
|
||||||
|
// Re-establish event handlers for all polyline groups
|
||||||
|
polylinesLayer.eachLayer((groupLayer) => {
|
||||||
|
if (groupLayer instanceof L.LayerGroup || groupLayer instanceof L.FeatureGroup) {
|
||||||
|
groupsProcessed++;
|
||||||
|
|
||||||
|
let segments = [];
|
||||||
|
|
||||||
|
groupLayer.eachLayer((segment) => {
|
||||||
|
if (segment instanceof L.Polyline) {
|
||||||
|
segments.push(segment);
|
||||||
|
segmentsProcessed++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If we have stored polyline coordinates, use them; otherwise create a basic representation
|
||||||
|
let polylineCoordinates = groupLayer._polylineCoordinates || [];
|
||||||
|
|
||||||
|
if (polylineCoordinates.length === 0) {
|
||||||
|
// Fallback: reconstruct coordinates from segments
|
||||||
|
const coordsMap = new Map();
|
||||||
|
segments.forEach(segment => {
|
||||||
|
const coords = segment.getLatLngs();
|
||||||
|
coords.forEach(coord => {
|
||||||
|
const key = `${coord.lat.toFixed(6)},${coord.lng.toFixed(6)}`;
|
||||||
|
if (!coordsMap.has(key)) {
|
||||||
|
const timestamp = segment.options.timestamp || Date.now() / 1000;
|
||||||
|
const speed = segment.options.speed || 0;
|
||||||
|
coordsMap.set(key, [coord.lat, coord.lng, 0, 0, timestamp, speed]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
polylineCoordinates = Array.from(coordsMap.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-establish the highlight hover functionality
|
||||||
|
if (polylineCoordinates.length > 0) {
|
||||||
|
addHighlightOnHover(groupLayer, map, polylineCoordinates, userSettings, distanceUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-establish basic group event handlers
|
||||||
|
groupLayer.on('mouseover', function(e) {
|
||||||
|
L.DomEvent.stopPropagation(e);
|
||||||
|
segments.forEach(segment => {
|
||||||
|
segment.setStyle({
|
||||||
|
weight: 8,
|
||||||
|
opacity: 1
|
||||||
|
});
|
||||||
|
if (map.hasLayer(segment)) {
|
||||||
|
segment.bringToFront();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
groupLayer.on('mouseout', function(e) {
|
||||||
|
L.DomEvent.stopPropagation(e);
|
||||||
|
segments.forEach(segment => {
|
||||||
|
segment.setStyle({
|
||||||
|
weight: 3,
|
||||||
|
opacity: userSettings.route_opacity,
|
||||||
|
color: segment.options.originalColor
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
groupLayer.on('click', function(e) {
|
||||||
|
// Click handler placeholder
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure the group is interactive
|
||||||
|
groupLayer.options.interactive = true;
|
||||||
|
groupLayer.options.bubblingMouseEvents = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function managePaneVisibility(map, activeLayerType) {
|
||||||
|
const polylinesPane = map.getPane('polylinesPane');
|
||||||
|
const tracksPane = map.getPane('tracksPane');
|
||||||
|
|
||||||
|
if (activeLayerType === 'routes') {
|
||||||
|
// Enable polylines pane events and disable tracks pane events
|
||||||
|
if (polylinesPane) {
|
||||||
|
polylinesPane.style.pointerEvents = 'auto';
|
||||||
|
polylinesPane.style.zIndex = 470; // Temporarily boost above tracks
|
||||||
|
}
|
||||||
|
if (tracksPane) {
|
||||||
|
tracksPane.style.pointerEvents = 'none';
|
||||||
|
}
|
||||||
|
} else if (activeLayerType === 'tracks') {
|
||||||
|
// Enable tracks pane events and disable polylines pane events
|
||||||
|
if (tracksPane) {
|
||||||
|
tracksPane.style.pointerEvents = 'auto';
|
||||||
|
tracksPane.style.zIndex = 470; // Boost above polylines
|
||||||
|
}
|
||||||
|
if (polylinesPane) {
|
||||||
|
polylinesPane.style.pointerEvents = 'none';
|
||||||
|
polylinesPane.style.zIndex = 450; // Reset to original
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Both layers might be active or neither - enable both
|
||||||
|
if (polylinesPane) {
|
||||||
|
polylinesPane.style.pointerEvents = 'auto';
|
||||||
|
polylinesPane.style.zIndex = 450; // Reset to original
|
||||||
|
}
|
||||||
|
if (tracksPane) {
|
||||||
|
tracksPane.style.pointerEvents = 'auto';
|
||||||
|
tracksPane.style.zIndex = 460; // Reset to original
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,9 @@ export function addTrackInteractions(trackGroup, map, track, userSettings, dista
|
||||||
const endMarker = L.marker([endCoord[0], endCoord[1]], { icon: endIcon });
|
const endMarker = L.marker([endCoord[0], endCoord[1]], { icon: endIcon });
|
||||||
|
|
||||||
function handleTrackHover(e) {
|
function handleTrackHover(e) {
|
||||||
if (isClicked) return; // Don't change hover state if clicked
|
if (isClicked) {
|
||||||
|
return; // Don't change hover state if clicked
|
||||||
|
}
|
||||||
|
|
||||||
// Apply hover style to all segments in the track
|
// Apply hover style to all segments in the track
|
||||||
trackGroup.eachLayer((layer) => {
|
trackGroup.eachLayer((layer) => {
|
||||||
|
|
@ -185,36 +187,22 @@ export function addTrackInteractions(trackGroup, map, track, userSettings, dista
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTrackCoordinates(track) {
|
function getTrackCoordinates(track) {
|
||||||
// Add debugging to see what we're working with
|
|
||||||
console.log(`DEBUG: Parsing track ${track.id}:`, {
|
|
||||||
has_coordinates: !!(track.coordinates && Array.isArray(track.coordinates)),
|
|
||||||
has_path: !!(track.path && Array.isArray(track.path)),
|
|
||||||
original_path_type: typeof track.original_path,
|
|
||||||
original_path_length: track.original_path ? track.original_path.length : 0,
|
|
||||||
original_path_sample: track.original_path ? track.original_path.substring(0, 100) + '...' : null
|
|
||||||
});
|
|
||||||
|
|
||||||
// First check if coordinates are already provided as an array
|
// First check if coordinates are already provided as an array
|
||||||
if (track.coordinates && Array.isArray(track.coordinates)) {
|
if (track.coordinates && Array.isArray(track.coordinates)) {
|
||||||
console.log(`DEBUG: Using coordinates array for track ${track.id}`);
|
|
||||||
return track.coordinates; // If already provided as array of [lat, lng]
|
return track.coordinates; // If already provided as array of [lat, lng]
|
||||||
}
|
}
|
||||||
|
|
||||||
// If coordinates are provided as a path property
|
// If coordinates are provided as a path property
|
||||||
if (track.path && Array.isArray(track.path)) {
|
if (track.path && Array.isArray(track.path)) {
|
||||||
console.log(`DEBUG: Using path array for track ${track.id}`);
|
|
||||||
return track.path;
|
return track.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to parse from original_path (PostGIS LineString format)
|
// Try to parse from original_path (PostGIS LineString format)
|
||||||
if (track.original_path && typeof track.original_path === 'string') {
|
if (track.original_path && typeof track.original_path === 'string') {
|
||||||
try {
|
try {
|
||||||
console.log(`DEBUG: Attempting to parse original_path for track ${track.id}: "${track.original_path}"`);
|
|
||||||
|
|
||||||
// Parse PostGIS LineString format: "LINESTRING (lng lat, lng lat, ...)" or "LINESTRING(lng lat, lng lat, ...)"
|
// Parse PostGIS LineString format: "LINESTRING (lng lat, lng lat, ...)" or "LINESTRING(lng lat, lng lat, ...)"
|
||||||
const match = track.original_path.match(/LINESTRING\s*\(([^)]+)\)/i);
|
const match = track.original_path.match(/LINESTRING\s*\(([^)]+)\)/i);
|
||||||
if (match) {
|
if (match) {
|
||||||
console.log(`DEBUG: LineString match found for track ${track.id}: "${match[1]}"`);
|
|
||||||
const coordString = match[1];
|
const coordString = match[1];
|
||||||
const coordinates = coordString.split(',').map(pair => {
|
const coordinates = coordString.split(',').map(pair => {
|
||||||
const [lng, lat] = pair.trim().split(/\s+/).map(parseFloat);
|
const [lng, lat] = pair.trim().split(/\s+/).map(parseFloat);
|
||||||
|
|
@ -225,8 +213,6 @@ function getTrackCoordinates(track) {
|
||||||
return [lat, lng]; // Return as [lat, lng] for Leaflet
|
return [lat, lng]; // Return as [lat, lng] for Leaflet
|
||||||
}).filter(Boolean); // Remove null entries
|
}).filter(Boolean); // Remove null entries
|
||||||
|
|
||||||
console.log(`DEBUG: Parsed ${coordinates.length} coordinates for track ${track.id}`);
|
|
||||||
|
|
||||||
if (coordinates.length >= 2) {
|
if (coordinates.length >= 2) {
|
||||||
return coordinates;
|
return coordinates;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -243,7 +229,6 @@ function getTrackCoordinates(track) {
|
||||||
|
|
||||||
// For development/testing, create a simple line if we have start/end coordinates
|
// For development/testing, create a simple line if we have start/end coordinates
|
||||||
if (track.start_point && track.end_point) {
|
if (track.start_point && track.end_point) {
|
||||||
console.log(`DEBUG: Using start/end points for track ${track.id}`);
|
|
||||||
return [
|
return [
|
||||||
[track.start_point.lat, track.start_point.lng],
|
[track.start_point.lat, track.start_point.lng],
|
||||||
[track.end_point.lat, track.end_point.lng]
|
[track.end_point.lat, track.end_point.lng]
|
||||||
|
|
|
||||||
81
app/models/concerns/calculateable.rb
Normal file
81
app/models/concerns/calculateable.rb
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Calculateable
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
def calculate_path
|
||||||
|
updated_path = build_path_from_coordinates
|
||||||
|
set_path_attributes(updated_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_distance
|
||||||
|
calculated_distance = calculate_distance_from_coordinates
|
||||||
|
self.distance = convert_distance_for_storage(calculated_distance)
|
||||||
|
end
|
||||||
|
|
||||||
|
def recalculate_path!
|
||||||
|
calculate_path
|
||||||
|
save_if_changed!
|
||||||
|
end
|
||||||
|
|
||||||
|
def recalculate_distance!
|
||||||
|
calculate_distance
|
||||||
|
save_if_changed!
|
||||||
|
end
|
||||||
|
|
||||||
|
def recalculate_path_and_distance!
|
||||||
|
calculate_path
|
||||||
|
calculate_distance
|
||||||
|
save_if_changed!
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def path_coordinates
|
||||||
|
points.pluck(:lonlat)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_path_from_coordinates
|
||||||
|
Tracks::BuildPath.new(path_coordinates).call
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_path_attributes(updated_path)
|
||||||
|
self.path = updated_path if respond_to?(:path=)
|
||||||
|
self.original_path = updated_path if respond_to?(:original_path=)
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_distance_unit
|
||||||
|
user.safe_settings.distance_unit
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_distance_from_coordinates
|
||||||
|
Point.total_distance(points, user_distance_unit)
|
||||||
|
end
|
||||||
|
|
||||||
|
def convert_distance_for_storage(calculated_distance)
|
||||||
|
if track_model?
|
||||||
|
convert_distance_to_meters(calculated_distance)
|
||||||
|
else
|
||||||
|
# For Trip model - store rounded distance in user's preferred unit
|
||||||
|
calculated_distance.round
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def track_model?
|
||||||
|
self.class.name == 'Track'
|
||||||
|
end
|
||||||
|
|
||||||
|
def convert_distance_to_meters(calculated_distance)
|
||||||
|
# For Track model - convert to meters for storage (Track expects distance in meters)
|
||||||
|
case user_distance_unit.to_s
|
||||||
|
when 'miles', 'mi'
|
||||||
|
(calculated_distance * 1609.344).round(2) # miles to meters
|
||||||
|
else
|
||||||
|
(calculated_distance * 1000).round(2) # km to meters
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_if_changed!
|
||||||
|
save! if changed?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -33,6 +33,7 @@ class Point < ApplicationRecord
|
||||||
after_create :async_reverse_geocode, if: -> { DawarichSettings.store_geodata? && !reverse_geocoded? }
|
after_create :async_reverse_geocode, if: -> { DawarichSettings.store_geodata? && !reverse_geocoded? }
|
||||||
after_create :set_country
|
after_create :set_country
|
||||||
after_create_commit :broadcast_coordinates
|
after_create_commit :broadcast_coordinates
|
||||||
|
after_commit :recalculate_track, on: :update
|
||||||
|
|
||||||
def self.without_raw_data
|
def self.without_raw_data
|
||||||
select(column_names - ['raw_data'])
|
select(column_names - ['raw_data'])
|
||||||
|
|
@ -93,4 +94,10 @@ class Point < ApplicationRecord
|
||||||
# Safely get country name from association or attribute
|
# Safely get country name from association or attribute
|
||||||
self.country&.name || read_attribute(:country) || ''
|
self.country&.name || read_attribute(:country) || ''
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def recalculate_track
|
||||||
|
return unless track.present?
|
||||||
|
|
||||||
|
track.recalculate_path_and_distance!
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Track < ApplicationRecord
|
class Track < ApplicationRecord
|
||||||
|
include Calculateable
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
has_many :points, dependent: :nullify
|
has_many :points, dependent: :nullify
|
||||||
|
|
||||||
validates :start_at, :end_at, :original_path, presence: true
|
validates :start_at, :end_at, :original_path, presence: true
|
||||||
validates :distance, :avg_speed, :duration, numericality: { greater_than: 0 }
|
validates :distance, :avg_speed, :duration, numericality: { greater_than: 0 }
|
||||||
validates :elevation_gain, :elevation_loss, :elevation_max, :elevation_min,
|
|
||||||
numericality: { greater_than_or_equal_to: 0 }
|
|
||||||
|
|
||||||
def calculate_path
|
after_update :recalculate_path_and_distance!, if: -> { points.exists? && (saved_change_to_start_at? || saved_change_to_end_at?) }
|
||||||
Tracks::BuildPath.new(points.pluck(:lonlat)).call
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Trip < ApplicationRecord
|
class Trip < ApplicationRecord
|
||||||
|
include Calculateable
|
||||||
|
|
||||||
has_rich_text :notes
|
has_rich_text :notes
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
|
@ -32,17 +34,7 @@ class Trip < ApplicationRecord
|
||||||
@photo_sources ||= photos.map { _1[:source] }.uniq
|
@photo_sources ||= photos.map { _1[:source] }.uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
def calculate_path
|
|
||||||
trip_path = Tracks::BuildPath.new(points.pluck(:lonlat)).call
|
|
||||||
|
|
||||||
self.path = trip_path
|
|
||||||
end
|
|
||||||
|
|
||||||
def calculate_distance
|
|
||||||
distance = Point.total_distance(points, user.safe_settings.distance_unit)
|
|
||||||
|
|
||||||
self.distance = distance.round
|
|
||||||
end
|
|
||||||
|
|
||||||
def calculate_countries
|
def calculate_countries
|
||||||
countries =
|
countries =
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,43 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class TrackSerializer
|
class TrackSerializer
|
||||||
def initialize(user, start_at, end_at)
|
def initialize(user, coordinates)
|
||||||
@user = user
|
@user = user
|
||||||
@start_at = start_at
|
@coordinates = coordinates
|
||||||
@end_at = end_at
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def call
|
def call
|
||||||
|
# Extract track IDs from the coordinates that are already filtered by timeframe
|
||||||
|
track_ids = extract_track_ids_from_coordinates
|
||||||
|
return [] if track_ids.empty?
|
||||||
|
|
||||||
|
# Show only tracks that have points in the selected timeframe
|
||||||
tracks_data = user.tracks
|
tracks_data = user.tracks
|
||||||
.where('start_at <= ? AND end_at >= ?', Time.zone.at(end_at), Time.zone.at(start_at))
|
.where(id: track_ids)
|
||||||
.order(start_at: :asc)
|
.order(start_at: :asc)
|
||||||
.pluck(:id, :start_at, :end_at, :distance, :avg_speed, :duration,
|
.pluck(:id, :start_at, :end_at, :distance, :avg_speed, :duration,
|
||||||
:elevation_gain, :elevation_loss, :elevation_max, :elevation_min, :original_path)
|
:elevation_gain, :elevation_loss, :elevation_max, :elevation_min, :original_path)
|
||||||
|
|
||||||
tracks_data.map do |id, start_at, end_at, distance, avg_speed, duration,
|
tracks_data.map do |id, start_at, end_at, distance, avg_speed, duration,
|
||||||
elevation_gain, elevation_loss, elevation_max, elevation_min, original_path|
|
elevation_gain, elevation_loss, elevation_max, elevation_min, original_path|
|
||||||
serialize_track_data(id, start_at, end_at, distance, avg_speed, duration,
|
serialize_track_data(
|
||||||
elevation_gain, elevation_loss, elevation_max, elevation_min, original_path)
|
id, start_at, end_at, distance, avg_speed, duration, elevation_gain,
|
||||||
|
elevation_loss, elevation_max, elevation_min, original_path
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
attr_reader :user, :start_at, :end_at
|
attr_reader :user, :coordinates
|
||||||
|
|
||||||
|
def extract_track_ids_from_coordinates
|
||||||
|
# Extract track_id from coordinates (index 8: [lat, lng, battery, altitude, timestamp, velocity, id, country, track_id])
|
||||||
|
track_ids = coordinates.map { |coord| coord[8]&.to_i }.compact.uniq
|
||||||
|
track_ids.reject(&:zero?) # Remove any nil/zero track IDs
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_track_data(
|
def serialize_track_data(
|
||||||
id, start_at, end_at, distance, avg_speed, duration, elevation_gain,
|
id, start_at, end_at, distance, avg_speed, duration, elevation_gain,
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,26 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Tracks::CreateFromPoints
|
class Tracks::CreateFromPoints
|
||||||
attr_reader :user, :distance_threshold_meters, :time_threshold_minutes
|
attr_reader :user, :distance_threshold_meters, :time_threshold_minutes, :start_at, :end_at
|
||||||
|
|
||||||
def initialize(user)
|
def initialize(user, start_at: nil, end_at: nil)
|
||||||
@user = user
|
@user = user
|
||||||
@distance_threshold_meters = user.safe_settings.meters_between_routes || 500
|
@start_at = start_at
|
||||||
@time_threshold_minutes = user.safe_settings.minutes_between_routes || 30
|
@end_at = end_at
|
||||||
|
@distance_threshold_meters = user.safe_settings.meters_between_routes.to_i || 500
|
||||||
|
@time_threshold_minutes = user.safe_settings.minutes_between_routes.to_i || 60
|
||||||
end
|
end
|
||||||
|
|
||||||
def call
|
def call
|
||||||
Rails.logger.info "Creating tracks for user #{user.id} with thresholds: #{distance_threshold_meters}m, #{time_threshold_minutes}min"
|
time_range_info = start_at || end_at ? " for time range #{start_at} - #{end_at}" : ""
|
||||||
|
Rails.logger.info "Creating tracks for user #{user.id} with thresholds: #{distance_threshold_meters}m, #{time_threshold_minutes}min#{time_range_info}"
|
||||||
|
|
||||||
tracks_created = 0
|
tracks_created = 0
|
||||||
|
|
||||||
Track.transaction do
|
Track.transaction do
|
||||||
# Clear existing tracks for this user to regenerate them
|
# Clear existing tracks for this user (optionally scoped to time range)
|
||||||
user.tracks.destroy_all
|
tracks_to_delete = start_at || end_at ? scoped_tracks_for_deletion : user.tracks
|
||||||
|
tracks_to_delete.destroy_all
|
||||||
|
|
||||||
track_segments = split_points_into_tracks
|
track_segments = split_points_into_tracks
|
||||||
|
|
||||||
|
|
@ -28,17 +32,36 @@ class Tracks::CreateFromPoints
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Rails.logger.info "Created #{tracks_created} tracks for user #{user.id}"
|
Rails.logger.info "Created #{tracks_created} tracks for user #{user.id}#{time_range_info}"
|
||||||
tracks_created
|
tracks_created
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def user_points
|
def user_points
|
||||||
@user_points ||= Point.where(user: user)
|
@user_points ||= begin
|
||||||
.where.not(lonlat: nil)
|
points = Point.where(user: user)
|
||||||
.where.not(timestamp: nil)
|
.where.not(lonlat: nil)
|
||||||
.order(:timestamp)
|
.where.not(timestamp: nil)
|
||||||
|
|
||||||
|
# Apply timestamp filtering if provided
|
||||||
|
if start_at.present?
|
||||||
|
points = points.where('timestamp >= ?', start_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
if end_at.present?
|
||||||
|
points = points.where('timestamp <= ?', end_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
points.order(:timestamp)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def scoped_tracks_for_deletion
|
||||||
|
user.tracks.where(
|
||||||
|
'start_at <= ? AND end_at >= ?',
|
||||||
|
Time.zone.at(end_at), Time.zone.at(start_at)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def split_points_into_tracks
|
def split_points_into_tracks
|
||||||
|
|
@ -47,7 +70,9 @@ class Tracks::CreateFromPoints
|
||||||
track_segments = []
|
track_segments = []
|
||||||
current_segment = []
|
current_segment = []
|
||||||
|
|
||||||
user_points.find_each do |point|
|
# Use .each instead of find_each to preserve sequential processing
|
||||||
|
# find_each processes in batches which breaks track segmentation logic
|
||||||
|
user_points.each do |point|
|
||||||
if should_start_new_track?(point, current_segment.last)
|
if should_start_new_track?(point, current_segment.last)
|
||||||
# Finalize current segment if it has enough points
|
# Finalize current segment if it has enough points
|
||||||
track_segments << current_segment if current_segment.size >= 2
|
track_segments << current_segment if current_segment.size >= 2
|
||||||
|
|
@ -72,26 +97,22 @@ class Tracks::CreateFromPoints
|
||||||
|
|
||||||
return true if time_diff_seconds > time_threshold_seconds
|
return true if time_diff_seconds > time_threshold_seconds
|
||||||
|
|
||||||
# Check distance threshold
|
# Check distance threshold - convert km to meters to match frontend logic
|
||||||
distance_meters = calculate_distance_meters(previous_point, current_point)
|
distance_km = calculate_distance_kilometers(previous_point, current_point)
|
||||||
return true if distance_meters > distance_threshold_meters.to_i
|
distance_meters = distance_km * 1000 # Convert km to meters
|
||||||
|
return true if distance_meters > distance_threshold_meters
|
||||||
|
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
def calculate_distance_meters(point1, point2)
|
def calculate_distance_kilometers(point1, point2)
|
||||||
# Use PostGIS to calculate distance in meters
|
# Use Geocoder to match behavior with frontend (same library used elsewhere in app)
|
||||||
distance_query = <<-SQL.squish
|
Geocoder::Calculations.distance_between(
|
||||||
SELECT ST_Distance(
|
[point1.lat, point1.lon], [point2.lat, point2.lon], units: :km
|
||||||
ST_GeomFromEWKT($1)::geography,
|
)
|
||||||
ST_GeomFromEWKT($2)::geography
|
|
||||||
)
|
|
||||||
SQL
|
|
||||||
|
|
||||||
Point.connection.select_value(distance_query, nil, [point1.lonlat, point2.lonlat]).to_f
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_track_from_points(points)
|
def create_track_from_points(points)
|
||||||
track = Track.new(
|
track = Track.new(
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
start_at: Time.zone.at(points.first.timestamp),
|
start_at: Time.zone.at(points.first.timestamp),
|
||||||
|
|
@ -111,7 +132,7 @@ class Tracks::CreateFromPoints
|
||||||
track.elevation_max = elevation_stats[:max]
|
track.elevation_max = elevation_stats[:max]
|
||||||
track.elevation_min = elevation_stats[:min]
|
track.elevation_min = elevation_stats[:min]
|
||||||
|
|
||||||
if track.save
|
if track.save!
|
||||||
Point.where(id: points.map(&:id)).update_all(track_id: track.id)
|
Point.where(id: points.map(&:id)).update_all(track_id: track.id)
|
||||||
|
|
||||||
track
|
track
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,80 @@ RSpec.describe Track, type: :model do
|
||||||
it { is_expected.to validate_numericality_of(:distance).is_greater_than(0) }
|
it { is_expected.to validate_numericality_of(:distance).is_greater_than(0) }
|
||||||
it { is_expected.to validate_numericality_of(:avg_speed).is_greater_than(0) }
|
it { is_expected.to validate_numericality_of(:avg_speed).is_greater_than(0) }
|
||||||
it { is_expected.to validate_numericality_of(:duration).is_greater_than(0) }
|
it { is_expected.to validate_numericality_of(:duration).is_greater_than(0) }
|
||||||
it { is_expected.to validate_numericality_of(:elevation_gain).is_greater_than(0) }
|
end
|
||||||
it { is_expected.to validate_numericality_of(:elevation_loss).is_greater_than(0) }
|
|
||||||
it { is_expected.to validate_numericality_of(:elevation_max).is_greater_than(0) }
|
describe 'Calculateable concern' do
|
||||||
it { is_expected.to validate_numericality_of(:elevation_min).is_greater_than(0) }
|
let(:user) { create(:user) }
|
||||||
|
let(:track) { create(:track, user: user, distance: 1000, avg_speed: 25, duration: 3600) }
|
||||||
|
let!(:points) do
|
||||||
|
[
|
||||||
|
create(:point, user: user, track: track, lonlat: 'POINT(13.404954 52.520008)', timestamp: 1.hour.ago.to_i),
|
||||||
|
create(:point, user: user, track: track, lonlat: 'POINT(13.404955 52.520009)', timestamp: 30.minutes.ago.to_i),
|
||||||
|
create(:point, user: user, track: track, lonlat: 'POINT(13.404956 52.520010)', timestamp: Time.current.to_i)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#calculate_path' do
|
||||||
|
it 'updates the original_path with calculated path' do
|
||||||
|
original_path_before = track.original_path
|
||||||
|
track.calculate_path
|
||||||
|
|
||||||
|
expect(track.original_path).not_to eq(original_path_before)
|
||||||
|
expect(track.original_path).to be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#calculate_distance' do
|
||||||
|
it 'updates the distance based on points' do
|
||||||
|
track.calculate_distance
|
||||||
|
|
||||||
|
expect(track.distance).to be > 0
|
||||||
|
expect(track.distance).to be_a(Float)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'stores distance in meters for Track model' do
|
||||||
|
allow(user).to receive(:safe_settings).and_return(double(distance_unit: 'km'))
|
||||||
|
allow(Point).to receive(:total_distance).and_return(1.5) # 1.5 km
|
||||||
|
|
||||||
|
track.calculate_distance
|
||||||
|
|
||||||
|
expect(track.distance).to eq(1500.0) # Should be in meters
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#recalculate_distance!' do
|
||||||
|
it 'recalculates and saves the distance' do
|
||||||
|
original_distance = track.distance
|
||||||
|
|
||||||
|
track.recalculate_distance!
|
||||||
|
|
||||||
|
track.reload
|
||||||
|
expect(track.distance).not_to eq(original_distance)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#recalculate_path!' do
|
||||||
|
it 'recalculates and saves the path' do
|
||||||
|
original_path = track.original_path
|
||||||
|
|
||||||
|
track.recalculate_path!
|
||||||
|
|
||||||
|
track.reload
|
||||||
|
expect(track.original_path).not_to eq(original_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#recalculate_path_and_distance!' do
|
||||||
|
it 'recalculates both path and distance and saves' do
|
||||||
|
original_distance = track.distance
|
||||||
|
original_path = track.original_path
|
||||||
|
|
||||||
|
track.recalculate_path_and_distance!
|
||||||
|
|
||||||
|
track.reload
|
||||||
|
expect(track.distance).not_to eq(original_distance)
|
||||||
|
expect(track.original_path).not_to eq(original_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -137,4 +137,49 @@ RSpec.describe Trip, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'Calculateable concern' do
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let(:trip) { create(:trip, user: user) }
|
||||||
|
let!(:points) do
|
||||||
|
[
|
||||||
|
create(:point, user: user, lonlat: 'POINT(13.404954 52.520008)', timestamp: trip.started_at.to_i + 1.hour),
|
||||||
|
create(:point, user: user, lonlat: 'POINT(13.404955 52.520009)', timestamp: trip.started_at.to_i + 2.hours),
|
||||||
|
create(:point, user: user, lonlat: 'POINT(13.404956 52.520010)', timestamp: trip.started_at.to_i + 3.hours)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#calculate_distance' do
|
||||||
|
it 'stores distance in user preferred unit for Trip model' do
|
||||||
|
allow(user).to receive(:safe_settings).and_return(double(distance_unit: 'km'))
|
||||||
|
allow(Point).to receive(:total_distance).and_return(2.5) # 2.5 km
|
||||||
|
|
||||||
|
trip.calculate_distance
|
||||||
|
|
||||||
|
expect(trip.distance).to eq(3) # Should be rounded, in km
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#recalculate_distance!' do
|
||||||
|
it 'recalculates and saves the distance' do
|
||||||
|
original_distance = trip.distance
|
||||||
|
|
||||||
|
trip.recalculate_distance!
|
||||||
|
|
||||||
|
trip.reload
|
||||||
|
expect(trip.distance).not_to eq(original_distance)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#recalculate_path!' do
|
||||||
|
it 'recalculates and saves the path' do
|
||||||
|
original_path = trip.path
|
||||||
|
|
||||||
|
trip.recalculate_path!
|
||||||
|
|
||||||
|
trip.reload
|
||||||
|
expect(trip.path).not_to eq(original_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue