mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 01:01:39 -05:00
Implement GPX imports
This commit is contained in:
parent
98211351b8
commit
747418c854
22 changed files with 6042 additions and 842 deletions
|
|
@ -1 +1 @@
|
|||
0.6.4
|
||||
0.7.0
|
||||
|
|
|
|||
25
CHANGELOG.md
25
CHANGELOG.md
|
|
@ -5,6 +5,31 @@ 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.7.0] — 2024-06-19
|
||||
|
||||
## The GPX MVP Release
|
||||
|
||||
This release introduces support for GPX files to be imported. Now you can import GPX files from your devices to Dawarich. The import process is the same as for other kinds of files, just select the GPX file instead and choose "gpx" as a source. Both single-segmented and multi-segmented GPX files are supported.
|
||||
|
||||
⚠️ BREAKING CHANGES: ⚠️
|
||||
|
||||
- `/api/v1/owntracks/points` endpoint is removed. Please use `/api/v1/points` endpoint to upload your points from OwnTracks mobile app instead.
|
||||
|
||||
### Added
|
||||
|
||||
- Support for GPX files to be imported.
|
||||
|
||||
### Changed
|
||||
|
||||
- Couple of unnecessary params were hidden from route popup and now can be shown using `?debug=true` query parameter. This is useful for debugging purposes.
|
||||
|
||||
### Removed
|
||||
|
||||
- `/exports/download` endpoint is removed. Now you can download your exports directly from the Exports page.
|
||||
- `/api/v1/owntracks/points` endpoint is removed.
|
||||
|
||||
---
|
||||
|
||||
## [0.6.4] — 2024-06-18
|
||||
|
||||
### Added
|
||||
|
|
|
|||
1
Gemfile
1
Gemfile
|
|
@ -9,7 +9,6 @@ gem 'chartkick'
|
|||
gem 'data_migrate'
|
||||
gem 'devise'
|
||||
gem 'geocoder'
|
||||
gem 'gpx'
|
||||
gem 'importmap-rails'
|
||||
gem 'oj'
|
||||
gem 'pg'
|
||||
|
|
|
|||
|
|
@ -137,9 +137,6 @@ GEM
|
|||
csv (>= 3.0.0)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
gpx (1.1.1)
|
||||
nokogiri (~> 1.7)
|
||||
rake
|
||||
hashdiff (1.1.0)
|
||||
i18n (1.14.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
|
|
@ -408,7 +405,6 @@ DEPENDENCIES
|
|||
ffaker
|
||||
foreman
|
||||
geocoder
|
||||
gpx
|
||||
importmap-rails
|
||||
oj
|
||||
pg
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,20 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# TODO: Deprecate in 1.0
|
||||
|
||||
class Api::V1::PointsController < ApplicationController
|
||||
skip_forgery_protection
|
||||
|
||||
def create
|
||||
Rails.logger.info 'This endpoint will be deprecated in 1.0. Use /api/v1/owntracks/points instead'
|
||||
Owntracks::PointCreatingJob.perform_later(point_params)
|
||||
|
||||
render json: {}, status: :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def point_params
|
||||
params.permit!
|
||||
end
|
||||
end
|
||||
|
|
@ -1,183 +1,44 @@
|
|||
import { Controller } from "@hotwired/stimulus"
|
||||
import L, { circleMarker } from "leaflet"
|
||||
import "leaflet.heat"
|
||||
import { Controller } from "@hotwired/stimulus";
|
||||
import L from "leaflet";
|
||||
import "leaflet.heat";
|
||||
|
||||
// Connects to data-controller="maps"
|
||||
export default class extends Controller {
|
||||
static targets = ["container"]
|
||||
static targets = ["container"];
|
||||
|
||||
connect() {
|
||||
console.log("Map controller connected")
|
||||
var markers = JSON.parse(this.element.dataset.coordinates)
|
||||
var center = markers[markers.length - 1] || JSON.parse(this.element.dataset.center)
|
||||
var center = (center === undefined) ? [52.514568, 13.350111] : center;
|
||||
var timezone = this.element.dataset.timezone;
|
||||
console.log("Map controller connected");
|
||||
|
||||
var map = L.map(this.containerTarget, {
|
||||
layers: [this.osmMapLayer(), this.osmHotMapLayer()]
|
||||
const markers = JSON.parse(this.element.dataset.coordinates);
|
||||
let center = markers[markers.length - 1] || JSON.parse(this.element.dataset.center);
|
||||
center = center === undefined ? [52.514568, 13.350111] : center;
|
||||
const timezone = this.element.dataset.timezone;
|
||||
|
||||
const map = L.map(this.containerTarget, {
|
||||
layers: [this.osmMapLayer(), this.osmHotMapLayer()],
|
||||
}).setView([center[0], center[1]], 14);
|
||||
|
||||
var markersArray = this.markersArray(markers);
|
||||
var markersLayer = L.layerGroup(markersArray);
|
||||
var heatmapMarkers = markers.map(element => [element[0], element[1], 0.3]); // lat, lon, intensity
|
||||
const markersArray = this.createMarkersArray(markers);
|
||||
const markersLayer = L.layerGroup(markersArray);
|
||||
const heatmapMarkers = markers.map((element) => [element[0], element[1], 0.3]);
|
||||
|
||||
// Function to calculate distance between two lat-lng points using Haversine formula
|
||||
function haversineDistance(lat1, lon1, lat2, lon2) {
|
||||
const toRad = x => x * Math.PI / 180;
|
||||
const R = 6371; // Radius of the Earth in kilometers
|
||||
const dLat = toRad(lat2 - lat1);
|
||||
const dLon = toRad(lon2 - lon1);
|
||||
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
|
||||
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
return R * c * 1000; // Distance in meters
|
||||
}
|
||||
const polylinesLayer = this.createPolylinesLayer(markers, map, timezone);
|
||||
const heatmapLayer = L.heatLayer(heatmapMarkers, { radius: 20 }).addTo(map);
|
||||
|
||||
function getURLParameter(name) {
|
||||
return new URLSearchParams(window.location.search).get(name);
|
||||
}
|
||||
|
||||
function minutesToDaysHoursMinutes(minutes) {
|
||||
var days = Math.floor(minutes / (24 * 60));
|
||||
var hours = Math.floor((minutes % (24 * 60)) / 60);
|
||||
var minutes = minutes % 60;
|
||||
var result = '';
|
||||
|
||||
if (days > 0) {
|
||||
result += days + 'd ';
|
||||
}
|
||||
|
||||
if (hours > 0) {
|
||||
result += hours + 'h ';
|
||||
}
|
||||
|
||||
if (minutes > 0) {
|
||||
result += minutes + 'min';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function addHighlightOnHover(polyline, map, startPoint, endPoint, prevPoint, nextPoint, timezone) {
|
||||
// Define the original and highlight styles
|
||||
const originalStyle = { color: 'blue', opacity: 0.6, weight: 3 };
|
||||
const highlightStyle = { color: 'yellow', opacity: 1, weight: 5 };
|
||||
|
||||
// Apply original style to the polyline initially
|
||||
polyline.setStyle(originalStyle);
|
||||
|
||||
// Create the popup content for the route
|
||||
var firstTimestamp = new Date(startPoint[4] * 1000).toLocaleString('en-GB', { timeZone: timezone });
|
||||
var lastTimestamp = new Date(endPoint[4] * 1000).toLocaleString('en-GB', { timeZone: timezone });
|
||||
// Make timeOnRoute look nice with split to days, hours and minutes
|
||||
|
||||
var minutes = Math.round((endPoint[4] - startPoint[4]) / 60);
|
||||
var timeOnRoute = minutesToDaysHoursMinutes(minutes);
|
||||
|
||||
// Calculate distances to previous and next points
|
||||
var distanceToPrev = prevPoint ? haversineDistance(prevPoint[0], prevPoint[1], startPoint[0], startPoint[1]) : 'N/A';
|
||||
var distanceToNext = nextPoint ? haversineDistance(endPoint[0], endPoint[1], nextPoint[0], nextPoint[1]) : 'N/A';
|
||||
|
||||
// Calculate time between routes
|
||||
var timeBetweenPrev = prevPoint ? Math.round((startPoint[4] - prevPoint[4]) / 60) : 'N/A';
|
||||
var timeBetweenNext = nextPoint ? Math.round((nextPoint[4] - endPoint[4]) / 60) : 'N/A';
|
||||
|
||||
// Create custom emoji icons
|
||||
const startIcon = L.divIcon({ html: '🚥', className: 'emoji-icon' });
|
||||
const finishIcon = L.divIcon({ html: '🏁', className: 'emoji-icon' });
|
||||
|
||||
// Create markers for the start and end points
|
||||
const startMarker = L.marker([startPoint[0], startPoint[1]], { icon: startIcon }).bindPopup(`Start: ${firstTimestamp}`);
|
||||
const endMarker = L.marker([endPoint[0], endPoint[1]], { icon: finishIcon }).bindPopup(`
|
||||
<b>Start:</b> ${firstTimestamp}<br>
|
||||
<b>End:</b> ${lastTimestamp}<br>
|
||||
<b>Duration:</b> ${timeOnRoute}<br>
|
||||
<b>Prev Route:</b> ${Math.round(distanceToPrev)}m and ${minutesToDaysHoursMinutes(timeBetweenPrev)} away<br>
|
||||
<b>Next Route:</b> ${Math.round(distanceToNext)}m and ${minutesToDaysHoursMinutes(timeBetweenNext)} away<br>
|
||||
`);
|
||||
|
||||
// Add mouseover event to highlight the polyline and show the start and end markers
|
||||
polyline.on('mouseover', function(e) {
|
||||
polyline.setStyle(highlightStyle);
|
||||
startMarker.addTo(map);
|
||||
endMarker.addTo(map).openPopup();
|
||||
});
|
||||
|
||||
// Add mouseout event to revert the polyline style and remove the start and end markers
|
||||
polyline.on('mouseout', function(e) {
|
||||
polyline.setStyle(originalStyle);
|
||||
map.closePopup();
|
||||
map.removeLayer(startMarker);
|
||||
map.removeLayer(endMarker);
|
||||
});
|
||||
}
|
||||
|
||||
var splitPolylines = [];
|
||||
var currentPolyline = [];
|
||||
var distanceThresholdMeters = parseInt(getURLParameter('meters_between_routes')) || 500;
|
||||
var timeThresholdMinutes = parseInt(getURLParameter('minutes_between_routes')) || 60;
|
||||
|
||||
// Process markers and split polylines based on the distance and time
|
||||
for (let i = 0, len = markers.length; i < len; i++) {
|
||||
if (currentPolyline.length === 0) {
|
||||
currentPolyline.push(markers[i]);
|
||||
} else {
|
||||
var lastPoint = currentPolyline[currentPolyline.length - 1];
|
||||
var currentPoint = markers[i];
|
||||
var distance = haversineDistance(lastPoint[0], lastPoint[1], currentPoint[0], currentPoint[1]);
|
||||
var timeDifference = (currentPoint[4] - lastPoint[4]) / 60; // Time difference in minutes
|
||||
|
||||
if (distance > distanceThresholdMeters || timeDifference > timeThresholdMinutes) {
|
||||
splitPolylines.push([...currentPolyline]); // Use spread operator to clone the array
|
||||
currentPolyline = [currentPoint];
|
||||
} else {
|
||||
currentPolyline.push(currentPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add the last polyline if it exists
|
||||
if (currentPolyline.length > 0) {
|
||||
splitPolylines.push(currentPolyline);
|
||||
}
|
||||
|
||||
// Assuming each polylineCoordinates is an array of objects with lat, lng, and timestamp properties
|
||||
var polylineLayers = splitPolylines.map((polylineCoordinates, index) => {
|
||||
// Extract lat-lng pairs for the polyline
|
||||
var latLngs = polylineCoordinates.map(point => [point[0], point[1]]);
|
||||
|
||||
// Create a polyline with the given coordinates
|
||||
var polyline = L.polyline(latLngs, { color: 'blue', opacity: 0.6, weight: 3 });
|
||||
|
||||
// Get the start and end points
|
||||
var startPoint = polylineCoordinates[0];
|
||||
var endPoint = polylineCoordinates[polylineCoordinates.length - 1];
|
||||
|
||||
// Get the previous and next points
|
||||
var prevPoint = index > 0 ? splitPolylines[index - 1][splitPolylines[index - 1].length - 1] : null;
|
||||
var nextPoint = index < splitPolylines.length - 1 ? splitPolylines[index + 1][0] : null;
|
||||
|
||||
// Add highlighting and popups on hover
|
||||
addHighlightOnHover(polyline, map, startPoint, endPoint, prevPoint, nextPoint, timezone);
|
||||
|
||||
return polyline;
|
||||
});
|
||||
|
||||
var polylinesLayer = L.layerGroup(polylineLayers).addTo(map);
|
||||
var heatmapLayer = L.heatLayer(heatmapMarkers, { radius: 20 }).addTo(map);
|
||||
|
||||
var controlsLayer = {
|
||||
"Points": markersLayer,
|
||||
"Polylines": polylinesLayer,
|
||||
"Heatmap": heatmapLayer
|
||||
const controlsLayer = {
|
||||
Points: markersLayer,
|
||||
Polylines: polylinesLayer,
|
||||
Heatmap: heatmapLayer,
|
||||
};
|
||||
|
||||
L.control.scale({
|
||||
position: 'bottomright', // The default position is 'bottomleft'
|
||||
metric: true, // Display metric scale
|
||||
imperial: false, // Display imperial scale
|
||||
maxWidth: 120 // Maximum width of the scale control in pixels
|
||||
}).addTo(map);
|
||||
L.control
|
||||
.scale({
|
||||
position: "bottomright",
|
||||
metric: true,
|
||||
imperial: false,
|
||||
maxWidth: 120,
|
||||
})
|
||||
.addTo(map);
|
||||
|
||||
L.control.layers(this.baseMaps(), controlsLayer).addTo(map);
|
||||
|
||||
|
|
@ -190,50 +51,35 @@ export default class extends Controller {
|
|||
}
|
||||
|
||||
osmMapLayer() {
|
||||
return L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
return L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap'
|
||||
})
|
||||
attribution: "© OpenStreetMap",
|
||||
});
|
||||
}
|
||||
|
||||
osmHotMapLayer() {
|
||||
return L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
|
||||
return L.tileLayer("https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png", {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap contributors, Tiles style by Humanitarian OpenStreetMap Team hosted by OpenStreetMap France'
|
||||
})
|
||||
attribution: "© OpenStreetMap contributors, Tiles style by Humanitarian OpenStreetMap Team hosted by OpenStreetMap France",
|
||||
});
|
||||
}
|
||||
|
||||
baseMaps() {
|
||||
return {
|
||||
"OpenStreetMap": this.osmMapLayer(),
|
||||
"OpenStreetMap.HOT": this.osmHotMapLayer()
|
||||
}
|
||||
OpenStreetMap: this.osmMapLayer(),
|
||||
"OpenStreetMap.HOT": this.osmHotMapLayer(),
|
||||
};
|
||||
}
|
||||
|
||||
controlsLayer() {
|
||||
return {
|
||||
"Points": this.markersLayer,
|
||||
"Polyline": this.polylineLayer
|
||||
}
|
||||
createMarkersArray(markersData) {
|
||||
return markersData.map((marker) => {
|
||||
const [lat, lon] = marker;
|
||||
const popupContent = this.createPopupContent(marker);
|
||||
return L.circleMarker([lat, lon], { radius: 4 }).bindPopup(popupContent);
|
||||
});
|
||||
}
|
||||
|
||||
markersArray(markers_data) {
|
||||
var markersArray = []
|
||||
|
||||
for (var i = 0; i < markers_data.length; i++) {
|
||||
var lat = markers_data[i][0];
|
||||
var lon = markers_data[i][1];
|
||||
|
||||
var popupContent = this.popupContent(markers_data[i]);
|
||||
var circleMarker = L.circleMarker([lat, lon], {radius: 4})
|
||||
|
||||
markersArray.push(circleMarker.bindPopup(popupContent).openPopup())
|
||||
}
|
||||
|
||||
return markersArray
|
||||
}
|
||||
|
||||
popupContent(marker) {
|
||||
createPopupContent(marker) {
|
||||
return `
|
||||
<b>Timestamp:</b> ${this.formatDate(marker[4])}<br>
|
||||
<b>Latitude:</b> ${marker[0]}<br>
|
||||
|
|
@ -245,30 +91,160 @@ export default class extends Controller {
|
|||
}
|
||||
|
||||
formatDate(timestamp) {
|
||||
let date = new Date(timestamp * 1000); // Multiply by 1000 because JavaScript works with milliseconds
|
||||
|
||||
let timezone = this.element.dataset.timezone;
|
||||
|
||||
return date.toLocaleString('en-GB', { timeZone: timezone });
|
||||
const date = new Date(timestamp * 1000);
|
||||
const timezone = this.element.dataset.timezone;
|
||||
return date.toLocaleString("en-GB", { timeZone: timezone });
|
||||
}
|
||||
|
||||
addTileLayer(map) {
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
attribution: "© <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a>",
|
||||
}).addTo(map);
|
||||
}
|
||||
|
||||
|
||||
addPolyline(map, markers) {
|
||||
var coordinates = markers.map(element => element.slice(0, 2));
|
||||
L.polyline(coordinates).addTo(map);
|
||||
}
|
||||
|
||||
addLastMarker(map, markers) {
|
||||
if (markers.length > 0) {
|
||||
var lastMarker = markers[markers.length - 1].slice(0, 2)
|
||||
const lastMarker = markers[markers.length - 1].slice(0, 2);
|
||||
L.marker(lastMarker).addTo(map);
|
||||
}
|
||||
}
|
||||
|
||||
haversineDistance(lat1, lon1, lat2, lon2) {
|
||||
const toRad = (x) => (x * Math.PI) / 180;
|
||||
const R = 6371; // Radius of the Earth in kilometers
|
||||
const dLat = toRad(lat2 - lat1);
|
||||
const dLon = toRad(lon2 - lon1);
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
|
||||
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
return R * c * 1000; // Distance in meters
|
||||
}
|
||||
|
||||
minutesToDaysHoursMinutes(minutes) {
|
||||
const days = Math.floor(minutes / (24 * 60));
|
||||
const hours = Math.floor((minutes % (24 * 60)) / 60);
|
||||
minutes = minutes % 60;
|
||||
let result = "";
|
||||
|
||||
if (days > 0) {
|
||||
result += `${days}d `;
|
||||
}
|
||||
|
||||
if (hours > 0) {
|
||||
result += `${hours}h `;
|
||||
}
|
||||
|
||||
if (minutes > 0) {
|
||||
result += `${minutes}min`;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
getUrlParameter(name) {
|
||||
return new URLSearchParams(window.location.search).get(name);
|
||||
}
|
||||
|
||||
addHighlightOnHover(polyline, map, startPoint, endPoint, prevPoint, nextPoint, timezone) {
|
||||
const originalStyle = { color: "blue", opacity: 0.6, weight: 3 };
|
||||
const highlightStyle = { color: "yellow", opacity: 1, weight: 5 };
|
||||
|
||||
polyline.setStyle(originalStyle);
|
||||
|
||||
const firstTimestamp = new Date(startPoint[4] * 1000).toLocaleString("en-GB", { timeZone: timezone });
|
||||
const lastTimestamp = new Date(endPoint[4] * 1000).toLocaleString("en-GB", { timeZone: timezone });
|
||||
|
||||
const minutes = Math.round((endPoint[4] - startPoint[4]) / 60);
|
||||
const timeOnRoute = this.minutesToDaysHoursMinutes(minutes);
|
||||
const distance = this.haversineDistance(startPoint[0], startPoint[1], endPoint[0], endPoint[1]);
|
||||
|
||||
const distanceToPrev = prevPoint ? this.haversineDistance(prevPoint[0], prevPoint[1], startPoint[0], startPoint[1]) : "N/A";
|
||||
const distanceToNext = nextPoint ? this.haversineDistance(endPoint[0], endPoint[1], nextPoint[0], nextPoint[1]) : "N/A";
|
||||
|
||||
const timeBetweenPrev = prevPoint ? Math.round((startPoint[4] - prevPoint[4]) / 60) : "N/A";
|
||||
const timeBetweenNext = nextPoint ? Math.round((nextPoint[4] - endPoint[4]) / 60) : "N/A";
|
||||
|
||||
const startIcon = L.divIcon({ html: "🚥", className: "emoji-icon" });
|
||||
const finishIcon = L.divIcon({ html: "🏁", className: "emoji-icon" });
|
||||
|
||||
const isDebugMode = this.getUrlParameter("debug") === "true";
|
||||
|
||||
let popupContent = `
|
||||
<b>Start:</b> ${firstTimestamp}<br>
|
||||
<b>End:</b> ${lastTimestamp}<br>
|
||||
<b>Duration:</b> ${timeOnRoute}<br>
|
||||
<b>Distance:</b> ${Math.round(distance)}m<br>
|
||||
`;
|
||||
|
||||
if (isDebugMode) {
|
||||
popupContent += `
|
||||
<b>Prev Route:</b> ${Math.round(distanceToPrev)}m and ${this.minutesToDaysHoursMinutes(timeBetweenPrev)} away<br>
|
||||
<b>Next Route:</b> ${Math.round(distanceToNext)}m and ${this.minutesToDaysHoursMinutes(timeBetweenNext)} away<br>
|
||||
`;
|
||||
}
|
||||
|
||||
const startMarker = L.marker([startPoint[0], startPoint[1]], { icon: startIcon }).bindPopup(`Start: ${firstTimestamp}`);
|
||||
const endMarker = L.marker([endPoint[0], endPoint[1]], { icon: finishIcon }).bindPopup(popupContent);
|
||||
|
||||
polyline.on("mouseover", function () {
|
||||
polyline.setStyle(highlightStyle);
|
||||
startMarker.addTo(map);
|
||||
endMarker.addTo(map).openPopup();
|
||||
});
|
||||
|
||||
polyline.on("mouseout", function () {
|
||||
polyline.setStyle(originalStyle);
|
||||
map.closePopup();
|
||||
map.removeLayer(startMarker);
|
||||
map.removeLayer(endMarker);
|
||||
});
|
||||
}
|
||||
|
||||
createPolylinesLayer(markers, map, timezone) {
|
||||
const splitPolylines = [];
|
||||
let currentPolyline = [];
|
||||
const distanceThresholdMeters = parseInt(this.getUrlParameter("meters_between_routes")) || 500;
|
||||
const timeThresholdMinutes = parseInt(this.getUrlParameter("minutes_between_routes")) || 60;
|
||||
|
||||
for (let i = 0, len = markers.length; i < len; i++) {
|
||||
if (currentPolyline.length === 0) {
|
||||
currentPolyline.push(markers[i]);
|
||||
} else {
|
||||
const lastPoint = currentPolyline[currentPolyline.length - 1];
|
||||
const currentPoint = markers[i];
|
||||
const distance = this.haversineDistance(lastPoint[0], lastPoint[1], currentPoint[0], currentPoint[1]);
|
||||
const timeDifference = (currentPoint[4] - lastPoint[4]) / 60;
|
||||
|
||||
if (distance > distanceThresholdMeters || timeDifference > timeThresholdMinutes) {
|
||||
splitPolylines.push([...currentPolyline]);
|
||||
currentPolyline = [currentPoint];
|
||||
} else {
|
||||
currentPolyline.push(currentPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentPolyline.length > 0) {
|
||||
splitPolylines.push(currentPolyline);
|
||||
}
|
||||
|
||||
return L.layerGroup(
|
||||
splitPolylines.map((polylineCoordinates, index) => {
|
||||
const latLngs = polylineCoordinates.map((point) => [point[0], point[1]]);
|
||||
const polyline = L.polyline(latLngs, { color: "blue", opacity: 0.6, weight: 3 });
|
||||
|
||||
const startPoint = polylineCoordinates[0];
|
||||
const endPoint = polylineCoordinates[polylineCoordinates.length - 1];
|
||||
const prevPoint = index > 0 ? splitPolylines[index - 1][splitPolylines[index - 1].length - 1] : null;
|
||||
const nextPoint = index < splitPolylines.length - 1 ? splitPolylines[index + 1][0] : null;
|
||||
|
||||
this.addHighlightOnHover(polyline, map, startPoint, endPoint, prevPoint, nextPoint, timezone);
|
||||
|
||||
return polyline;
|
||||
})
|
||||
).addTo(map);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,11 +20,11 @@ class ImportJob < ApplicationJob
|
|||
|
||||
def parser(source)
|
||||
case source
|
||||
when 'google_semantic_history' then GoogleMaps::SemanticHistoryParser
|
||||
when 'google_records' then GoogleMaps::RecordsParser
|
||||
when 'google_phone_takeout' then GoogleMaps::PhoneTakeoutParser
|
||||
when 'owntracks' then OwnTracks::ExportParser
|
||||
when 'gpx' then Gpx::TrackParser
|
||||
when 'google_semantic_history' then GoogleMaps::SemanticHistoryParser
|
||||
when 'google_records' then GoogleMaps::RecordsParser
|
||||
when 'google_phone_takeout' then GoogleMaps::PhoneTakeoutParser
|
||||
when 'owntracks' then OwnTracks::ExportParser
|
||||
when 'gpx' then Gpx::TrackParser
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,5 +8,5 @@ class Import < ApplicationRecord
|
|||
|
||||
include ImportUploader::Attachment(:raw)
|
||||
|
||||
enum source: { google_semantic_history: 0, owntracks: 1, google_records: 2, google_phone_takeout: 3, gpx: 3 }
|
||||
enum source: { google_semantic_history: 0, owntracks: 1, google_records: 2, google_phone_takeout: 3, gpx: 4 }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,5 +10,39 @@ class Gpx::TrackParser
|
|||
end
|
||||
|
||||
def call
|
||||
segments = json['gpx']['trk']['trkseg']
|
||||
|
||||
if segments.is_a?(Array)
|
||||
segments.each do |segment|
|
||||
segment['trkpt'].each { create_point(_1) }
|
||||
end
|
||||
else
|
||||
segments['trkpt'].each { create_point(_1) }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_point(point)
|
||||
return if point['lat'].blank? || point['lon'].blank? || point['time'].blank?
|
||||
return if point_exists?(point)
|
||||
|
||||
Point.create(
|
||||
latitude: point['lat'].to_d,
|
||||
longitude: point['lon'].to_d,
|
||||
altitude: point['ele'].to_i,
|
||||
timestamp: Time.parse(point['time']).to_i,
|
||||
import_id: import.id,
|
||||
user_id:
|
||||
)
|
||||
end
|
||||
|
||||
def point_exists?(point)
|
||||
Point.exists?(
|
||||
latitude: point['lat'].to_d,
|
||||
longitude: point['lon'].to_d,
|
||||
timestamp: Time.parse(point['time']).to_i,
|
||||
user_id:
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,12 +11,21 @@
|
|||
</div>
|
||||
<% end %>
|
||||
|
||||
<label class="form-control w-full max-w-xs my-5">
|
||||
<div class="label">
|
||||
<div class="form-control w-full max-w-xs">
|
||||
<label class="label">
|
||||
<span class="label-text">Select source</span>
|
||||
</label>
|
||||
<div class="space-y-2">
|
||||
<%= form.collection_radio_buttons :source, Import.sources.except('google_records'), :first, :first do |b| %>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer space-x-3">
|
||||
<%= b.radio_button(class: "radio radio-primary") %>
|
||||
<span class="label-text"><%= b.text.humanize %></span>
|
||||
</label>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= form.collection_radio_buttons :source, Import.sources.except('google_records'), :first, :first %>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="form-control w-full max-w-xs my-5">
|
||||
<div class="label">
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ Rails.application.routes.draw do
|
|||
mount Sidekiq::Web => '/sidekiq'
|
||||
|
||||
get 'settings/theme', to: 'settings#theme'
|
||||
get 'export/download', to: 'export#download'
|
||||
|
||||
resources :imports
|
||||
resources :exports, only: %i[index create destroy]
|
||||
|
|
|
|||
4494
spec/fixtures/files/gpx/gpx_track_multiple_segments.gpx
vendored
Normal file
4494
spec/fixtures/files/gpx/gpx_track_multiple_segments.gpx
vendored
Normal file
File diff suppressed because it is too large
Load diff
1239
spec/fixtures/files/gpx/gpx_track_single_segment.gpx
vendored
Normal file
1239
spec/fixtures/files/gpx/gpx_track_single_segment.gpx
vendored
Normal file
File diff suppressed because it is too large
Load diff
404
spec/fixtures/files/gpx/track.gpx
vendored
404
spec/fixtures/files/gpx/track.gpx
vendored
|
|
@ -1,404 +0,0 @@
|
|||
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
|
||||
<gpx version="1.1" creator="OsmAndRouterV2" xmlns="http://www.topografix.com/GPX/1/1" xmlns:osmand="https://osmand.net" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
|
||||
<metadata>
|
||||
<name>Thu 18 Apr 2024</name>
|
||||
<time>2024-05-25T20:18:02Z</time>
|
||||
<extensions>
|
||||
<osmand:desc>какое-то описание:)</osmand:desc>
|
||||
</extensions>
|
||||
</metadata>
|
||||
<rte>
|
||||
<rtept lat="37.1722699" lon="-3.5792405">
|
||||
<extensions>
|
||||
<osmand:profile>bicycle</osmand:profile>
|
||||
<osmand:trkpt_idx>0</osmand:trkpt_idx>
|
||||
</extensions>
|
||||
</rtept>
|
||||
<rtept lat="37.1774211" lon="-3.583397">
|
||||
<extensions>
|
||||
<osmand:profile>bicycle</osmand:profile>
|
||||
<osmand:trkpt_idx>36</osmand:trkpt_idx>
|
||||
</extensions>
|
||||
</rtept>
|
||||
<rtept lat="37.1762033" lon="-3.576145">
|
||||
<extensions>
|
||||
<osmand:profile>bicycle</osmand:profile>
|
||||
<osmand:trkpt_idx>75</osmand:trkpt_idx>
|
||||
</extensions>
|
||||
</rtept>
|
||||
</rte>
|
||||
<trk>
|
||||
<name>Thu 18 Apr 2024</name>
|
||||
<trkseg>
|
||||
<trkpt lat="37.172267" lon="-3.5792388">
|
||||
<ele>822.4</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1722839" lon="-3.5791934">
|
||||
<ele>822.8</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1722989" lon="-3.5791451">
|
||||
<ele>823</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1722925" lon="-3.5791263">
|
||||
<ele>823.2</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1722882" lon="-3.5791075">
|
||||
<ele>823.5</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1722861" lon="-3.5790834">
|
||||
<ele>823.8</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1722882" lon="-3.5790619">
|
||||
<ele>824</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1722925" lon="-3.5790405">
|
||||
<ele>824.2</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1723032" lon="-3.579019">
|
||||
<ele>824.5</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1723245" lon="-3.5789895">
|
||||
<ele>825</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1723523" lon="-3.5789734">
|
||||
<ele>825</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1723673" lon="-3.5789707">
|
||||
<ele>825</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1723865" lon="-3.578968">
|
||||
<ele>825</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1724036" lon="-3.5789734">
|
||||
<ele>824.8</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1724207" lon="-3.5789815">
|
||||
<ele>824.5</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1724442" lon="-3.5790029">
|
||||
<ele>824</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1724592" lon="-3.5790271">
|
||||
<ele>824</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1725062" lon="-3.5790405">
|
||||
<ele>823.5</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1725703" lon="-3.5791075">
|
||||
<ele>822.8</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1726045" lon="-3.5791478">
|
||||
<ele>822</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1726644" lon="-3.5791907">
|
||||
<ele>822</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1728161" lon="-3.5793194">
|
||||
<ele>824.5</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1730127" lon="-3.5795125">
|
||||
<ele>827</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1732115" lon="-3.579711">
|
||||
<ele>829.5</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1735342" lon="-3.5800597">
|
||||
<ele>831.2</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1739894" lon="-3.5805237">
|
||||
<ele>833.2</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1743036" lon="-3.5807893">
|
||||
<ele>836.5</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1747032" lon="-3.5811165">
|
||||
<ele>839.8</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1751905" lon="-3.5814598">
|
||||
<ele>842.5</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1756928" lon="-3.5817441">
|
||||
<ele>846.5</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1759791" lon="-3.5819587">
|
||||
<ele>847.2</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1766181" lon="-3.5825783">
|
||||
<ele>850.2</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1769665" lon="-3.5829189">
|
||||
<ele>851.1</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1771054" lon="-3.5830772">
|
||||
<ele>851.5</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1772336" lon="-3.5832381">
|
||||
<ele>852</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1774043" lon="-3.583422">
|
||||
<ele>852.8</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1774043" lon="-3.583422">
|
||||
<ele>852.8</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.177567" lon="-3.5835975">
|
||||
<ele>853.5</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.177785" lon="-3.5837907">
|
||||
<ele>850.8</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1779004" lon="-3.5838711">
|
||||
<ele>849</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1780051" lon="-3.5838524">
|
||||
<ele>849.8</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1780949" lon="-3.5837477">
|
||||
<ele>850</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1782188" lon="-3.5835546">
|
||||
<ele>844.2</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1782616" lon="-3.5834447">
|
||||
<ele>840</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1782658" lon="-3.5833266">
|
||||
<ele>839</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1782381" lon="-3.5831067">
|
||||
<ele>837</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1782145" lon="-3.5827956">
|
||||
<ele>840</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1781953" lon="-3.5824817">
|
||||
<ele>841</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1781718" lon="-3.5823235">
|
||||
<ele>841.2</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1781013" lon="-3.5821331">
|
||||
<ele>842</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.177817" lon="-3.5814008">
|
||||
<ele>843.5</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1777807" lon="-3.5813364">
|
||||
<ele>844</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.177302" lon="-3.580859">
|
||||
<ele>847.8</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1772208" lon="-3.5807517">
|
||||
<ele>850.2</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1771524" lon="-3.5806176">
|
||||
<ele>853</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1770904" lon="-3.5804486">
|
||||
<ele>853.5</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1769451" lon="-3.579931">
|
||||
<ele>860.2</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1769002" lon="-3.5798183">
|
||||
<ele>863.2</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1768532" lon="-3.5797298">
|
||||
<ele>863.5</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1766502" lon="-3.5794777">
|
||||
<ele>867.8</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1765049" lon="-3.5792336">
|
||||
<ele>868.5</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1763659" lon="-3.5790029">
|
||||
<ele>873.8</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1761928" lon="-3.5787642">
|
||||
<ele>878.2</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1760753" lon="-3.5785604">
|
||||
<ele>881.2</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1760112" lon="-3.5784531">
|
||||
<ele>882.2</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1759492" lon="-3.5782814">
|
||||
<ele>884</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1758894" lon="-3.5780293">
|
||||
<ele>887</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1758787" lon="-3.5778362">
|
||||
<ele>888.5</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1758936" lon="-3.5776269">
|
||||
<ele>890</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1759342" lon="-3.5774285">
|
||||
<ele>892.2</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1759556" lon="-3.5772192">
|
||||
<ele>894.2</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1759834" lon="-3.5769108">
|
||||
<ele>895.2</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1760112" lon="-3.5767633">
|
||||
<ele>895.5</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1761544" lon="-3.57631">
|
||||
<ele>898.8</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1761757" lon="-3.5762268">
|
||||
<ele>899.5</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<trkpt lat="37.1761993" lon="-3.5761433">
|
||||
<ele>900.4</ele>
|
||||
<extensions />
|
||||
</trkpt>
|
||||
<extensions>
|
||||
<osmand:route>
|
||||
<segment id="1133458846" length="3" startTrkptIdx="0" segmentTime="2.3" speed="3.92" turnType="C" skipTurn="true" types="0,1,2,3,4" pointTypes=";5,6;" names="34" />
|
||||
<segment id="673226861" length="8" startTrkptIdx="2" segmentTime="5.35" speed="2.87" turnType="RNDB3" turnAngle="-96.02" types="7,8,1,3,4" pointTypes=";;;;;;;" />
|
||||
<segment id="673226859" length="4" startTrkptIdx="9" segmentTime="1.13" speed="6.39" types="7,8,1,3,4" pointTypes=";;;9" />
|
||||
<segment id="180207805" length="5" startTrkptIdx="12" segmentTime="1.56" speed="6.39" types="7,8,1,3,10,11,12,13,14,15,16,17,18,4" pointTypes=";;19;;" names="36" />
|
||||
<segment id="1093359139" length="4" startTrkptIdx="16" segmentTime="3.11" speed="6.39" types="7,1,2,3,10,11,12,13,14,15,16,17,18,4" pointTypes=";9;;20" names="36" />
|
||||
<segment id="637480312" length="4" startTrkptIdx="19" segmentTime="36.12" speed="1.55" types="7,3,10,11,12,13,14,15,16,17,18,4" pointTypes="20;21,22,23,19;;" names="36" />
|
||||
<segment id="877556120" length="4" startTrkptIdx="22" segmentTime="31.27" speed="4.5" turnType="KR" turnLanes="+C,TSLL" turnAngle="-0.46" types="24,23,21,7,25,26,10,11,12,13,14,15,16,17,18,27,28" pointTypes="9;29;29;9" names="36,37" />
|
||||
<segment id="877556120" length="11" startTrkptIdx="25" segmentTime="100.92" speed="4.57" types="24,23,21,7,25,26,10,11,12,13,14,15,16,17,18,27,28" pointTypes="9;20;29;20;30;29;30;;30;30;" names="36,37" />
|
||||
<segment id="877556120" length="4" startTrkptIdx="36" segmentTime="11.76" speed="5.8" turnType="C" skipTurn="true" types="24,23,21,7,25,26,10,11,12,13,14,15,16,17,18,27,28" pointTypes=";;;20" names="36,37" />
|
||||
<segment id="877556120" length="5" startTrkptIdx="39" segmentTime="10.59" speed="5.5" turnType="TSLR" turnLanes="+TSLR,TSLL" turnAngle="37.18" types="24,23,21,7,25,26,10,11,12,13,14,15,16,17,18,27,28" pointTypes="20;30;;;" names="36,37" />
|
||||
<segment id="76389218" length="32" startTrkptIdx="43" segmentTime="221.59" speed="3.3" types="24,23,21,7,25,26,27,28" pointTypes=";;;29;30;29;30;20;20;;;29;;;30;;30;;;;9;19;19;9;9;19;;29;30;9;31;" names="37" />
|
||||
<segment id="76389218" length="2" startTrkptIdx="74" segmentTime="2.58" speed="3.04" types="24,23,21,7,25,26,27,28" pointTypes=";" names="37" />
|
||||
</osmand:route>
|
||||
<osmand:types>
|
||||
<type t="highway" v="residential" />
|
||||
<type t="lanes" v="1" />
|
||||
<type t="oneway" v="yes" />
|
||||
<type t="surface" v="asphalt" />
|
||||
<type t="osmand_highway_integrity_brouting" v="0" />
|
||||
<type t="direction" v="forward" />
|
||||
<type t="highway" v="give_way" />
|
||||
<type t="highway" v="unclassified" />
|
||||
<type t="junction" v="roundabout" />
|
||||
<type t="osmand_ele_decline_7" v="1" />
|
||||
<type t="route_hiking" v="" />
|
||||
<type t="route_hiking_1" v="" />
|
||||
<type t="route_hiking_1_osmc_stub_name" v="." />
|
||||
<type t="route_hiking_1_osmc_text" v="DG" />
|
||||
<type t="route_hiking_1_osmc_textcolor" v="black" />
|
||||
<type t="route_hiking_1_operator" v="Diputación de Granada" />
|
||||
<type t="route_hiking_1_website" v="https://www.turgranada.es/ruta/dehesa-del-generalife/" />
|
||||
<type t="route_hiking_1_network" v="lwn" />
|
||||
<type t="route_hiking_1_osmc_background" v="white_2" />
|
||||
<type t="osmand_ele_decline_11" v="1" />
|
||||
<type t="osmand_ele_decline_5" v="1" />
|
||||
<type t="foot" v="yes" />
|
||||
<type t="barrier" v="lift_gate" />
|
||||
<type t="bicycle" v="yes" />
|
||||
<type t="access" v="no" />
|
||||
<type t="horse" v="yes" />
|
||||
<type t="surface" v="fine_gravel" />
|
||||
<type t="osmand_highway_integrity_brouting" v="1" />
|
||||
<type t="motor_vehicle" v="private" />
|
||||
<type t="osmand_ele_decline_3" v="1" />
|
||||
<type t="osmand_ele_decline_1" v="1" />
|
||||
<type t="barrier" v="gate" />
|
||||
<type t="name" v="" />
|
||||
<type t="ref" v="" />
|
||||
<type t="name" v="Camino Viejo del Cementerio" />
|
||||
<type t="direction" v="" />
|
||||
<type t="route_hiking_1_name" v="Dehesa del Generalife" />
|
||||
<type t="name" v="Camino de la Silla del Moro" />
|
||||
</osmand:types>
|
||||
</extensions>
|
||||
</trkseg>
|
||||
</trk>
|
||||
<extensions>
|
||||
<osmand:show_arrows>false</osmand:show_arrows>
|
||||
<osmand:show_start_finish>true</osmand:show_start_finish>
|
||||
<osmand:split_interval>0.0</osmand:split_interval>
|
||||
<osmand:split_type>no_split</osmand:split_type>
|
||||
<osmand:line_3d_visualization_by_type>none</osmand:line_3d_visualization_by_type>
|
||||
<osmand:line_3d_visualization_wall_color_type>none</osmand:line_3d_visualization_wall_color_type>
|
||||
<osmand:line_3d_visualization_position_type>top</osmand:line_3d_visualization_position_type>
|
||||
</extensions>
|
||||
</gpx>
|
||||
|
|
@ -7,6 +7,14 @@ RSpec.describe Import, type: :model do
|
|||
end
|
||||
|
||||
describe 'enums' do
|
||||
it { is_expected.to define_enum_for(:source).with_values(google_semantic_history: 0, owntracks: 1, google_records: 2, google_phone_takeout: 3) }
|
||||
it do
|
||||
is_expected.to define_enum_for(:source).with_values(
|
||||
google_semantic_history: 0,
|
||||
owntracks: 1,
|
||||
google_records: 2,
|
||||
google_phone_takeout: 3,
|
||||
gpx: 4
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Api::V1::Points', type: :request do
|
||||
describe 'POST /api/v1/points' do
|
||||
context 'with valid params' do
|
||||
let(:params) do
|
||||
{ lat: 1.0, lon: 1.0, tid: 'test', tst: Time.current.to_i, topic: 'iPhone 12 pro' }
|
||||
end
|
||||
|
||||
it 'returns http success' do
|
||||
post api_v1_points_path, params: params
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
it 'enqueues a job' do
|
||||
expect do
|
||||
post api_v1_points_path, params: params
|
||||
end.to have_enqueued_job(Owntracks::PointCreatingJob)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Exports', type: :request do
|
||||
describe 'GET /download' do
|
||||
before do
|
||||
stub_request(:any, 'https://api.github.com/repos/Freika/dawarich/tags')
|
||||
.to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {})
|
||||
|
||||
sign_in create(:user)
|
||||
end
|
||||
|
||||
it 'returns a success response with a file' do
|
||||
get export_download_path
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response.headers['Content-Disposition']).to include('attachment')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -59,7 +59,7 @@ RSpec.describe 'Imports', type: :request do
|
|||
end
|
||||
|
||||
context 'when importing gpx data' do
|
||||
let(:file) { fixture_file_upload('gpx/track.gpx', 'application/gpx+xml') }
|
||||
let(:file) { fixture_file_upload('gpx/gpx_track_single_segment.gpx', 'application/gpx+xml') }
|
||||
|
||||
it 'queues import job' do
|
||||
expect do
|
||||
|
|
|
|||
30
spec/services/gpx/track_parser_spec.rb
Normal file
30
spec/services/gpx/track_parser_spec.rb
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Gpx::TrackParser do
|
||||
describe '#call' do
|
||||
subject(:parser) { described_class.new(import, user.id).call }
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:file_path) { Rails.root.join('spec/fixtures/files/gpx/gpx_track_single_segment.gpx') }
|
||||
let(:raw_data) { Hash.from_xml(File.read(file_path)) }
|
||||
let(:import) { create(:import, user:, name: 'gpx_track.gpx', raw_data:) }
|
||||
|
||||
context 'when file exists' do
|
||||
context 'when file has a single segment' do
|
||||
it 'creates points' do
|
||||
expect { parser }.to change { Point.count }.by(301)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when file has multiple segments' do
|
||||
let(:file_path) { Rails.root.join('spec/fixtures/files/gpx/gpx_track_multiple_segments.gpx') }
|
||||
|
||||
it 'creates points' do
|
||||
expect { parser }.to change { Point.count }.by(558)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'swagger_helper'
|
||||
|
||||
describe 'Points API', type: :request do
|
||||
path '/api/v1/points' do
|
||||
post 'Creates a point' do
|
||||
request_body_example value: {
|
||||
lat: 52.502397,
|
||||
lon: 13.356718,
|
||||
tid: 'Swagger',
|
||||
tst: Time.current.to_i
|
||||
}
|
||||
tags 'Points'
|
||||
consumes 'application/json'
|
||||
parameter name: :point, in: :body, schema: {
|
||||
type: :object,
|
||||
properties: {
|
||||
acc: { type: :number },
|
||||
alt: { type: :number },
|
||||
batt: { type: :number },
|
||||
bs: { type: :number },
|
||||
cog: { type: :number },
|
||||
lat: { type: :string, format: :decimal },
|
||||
lon: { type: :string, format: :decimal },
|
||||
rad: { type: :number },
|
||||
t: { type: :string },
|
||||
tid: { type: :string },
|
||||
tst: { type: :number },
|
||||
vac: { type: :number },
|
||||
vel: { type: :number },
|
||||
p: { type: :string, format: :decimal },
|
||||
poi: { type: :string },
|
||||
conn: { type: :string },
|
||||
tag: { type: :string },
|
||||
topic: { type: :string },
|
||||
inregions: { type: :array },
|
||||
SSID: { type: :string },
|
||||
BSSID: { type: :string },
|
||||
created_at: { type: :string },
|
||||
inrids: { type: :array },
|
||||
m: { type: :number }
|
||||
},
|
||||
required: %w[lat lon tid tst]
|
||||
}
|
||||
|
||||
response '200', 'point created' do
|
||||
let(:point) { { lat: 1.0, lon: 2.0, tid: 3, tst: 4 } }
|
||||
|
||||
run_test!
|
||||
end
|
||||
|
||||
response '200', 'invalid request' do
|
||||
let(:point) { { lat: 1.0 } }
|
||||
|
||||
run_test!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -102,85 +102,6 @@ paths:
|
|||
wifi: unknown
|
||||
battery_state: unknown
|
||||
battery_level: 0
|
||||
"/api/v1/points":
|
||||
post:
|
||||
summary: Creates a point
|
||||
tags:
|
||||
- Points
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: invalid request
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
acc:
|
||||
type: number
|
||||
alt:
|
||||
type: number
|
||||
batt:
|
||||
type: number
|
||||
bs:
|
||||
type: number
|
||||
cog:
|
||||
type: number
|
||||
lat:
|
||||
type: string
|
||||
format: decimal
|
||||
lon:
|
||||
type: string
|
||||
format: decimal
|
||||
rad:
|
||||
type: number
|
||||
t:
|
||||
type: string
|
||||
tid:
|
||||
type: string
|
||||
tst:
|
||||
type: number
|
||||
vac:
|
||||
type: number
|
||||
vel:
|
||||
type: number
|
||||
p:
|
||||
type: string
|
||||
format: decimal
|
||||
poi:
|
||||
type: string
|
||||
conn:
|
||||
type: string
|
||||
tag:
|
||||
type: string
|
||||
topic:
|
||||
type: string
|
||||
inregions:
|
||||
type: array
|
||||
SSID:
|
||||
type: string
|
||||
BSSID:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
inrids:
|
||||
type: array
|
||||
m:
|
||||
type: number
|
||||
required:
|
||||
- lat
|
||||
- lon
|
||||
- tid
|
||||
- tst
|
||||
examples:
|
||||
'0':
|
||||
summary: Creates a point
|
||||
value:
|
||||
lat: 52.502397
|
||||
lon: 13.356718
|
||||
tid: Swagger
|
||||
tst: 1718385215
|
||||
servers:
|
||||
- url: http://{defaultHost}
|
||||
variables:
|
||||
|
|
|
|||
Loading…
Reference in a new issue