Merge pull request #826 from Freika/dev

0.24.0
This commit is contained in:
Evgenii Burmakin 2025-02-10 19:02:22 +01:00 committed by GitHub
commit 6f3297173c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 778 additions and 8653 deletions

View file

@ -1 +1 @@
0.23.7
0.24.0

View file

@ -1,5 +1,5 @@
# Base-Image for Ruby and Node.js
FROM ruby:3.3.4-alpine
FROM ruby:3.4.1-alpine
ENV APP_PATH=/var/app
ENV BUNDLE_VERSION=2.5.21

View file

@ -35,7 +35,7 @@ jobs:
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3.4'
ruby-version: '3.4.1'
bundler-cache: true
- name: Set up Node.js

View file

@ -1,11 +1,54 @@
# Change Log
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## 0.23.7 - 2025-02-06
# 0.24.0 - 2025-02-09
## Points speed units
Dawarich expects speed to be sent in meters per second. It's already known that OwnTracks and GPSLogger (in some configurations) are sending speed in kilometers per hour.
In GPSLogger it's easily fixable: if you previously had `"vel": "%SPD_KMH"`, change it to `"vel": "%SPD"`, like it's described in the [docs](https://dawarich.app/docs/tutorials/track-your-location#gps-logger).
In OwnTracks it's a bit more complicated. You can't change the speed unit in the settings, so Dawarich will expect speed in kilometers per hour and will convert it to meters per second. Nothing is needed to be done from your side.
Now, we need to fix existing points with speed in kilometers per hour. The following guide assumes that you have been tracking your location exclusively with speed in kilometers per hour. If you have been using both speed units (say, were tracking with OwnTracks in kilometers per hour and with GPSLogger in meters per second), you need to decide what to do with points that have speed in kilometers per hour, as there is no easy way to distinguish them from points with speed in meters per second.
To convert speed in kilometers per hour to meters per second in your points, follow these steps:
1. Enter [Dawarich console](https://dawarich.app/docs/FAQ#how-to-enter-dawarich-console)
2. Run `points = Point.where(import_id: nil).where.not(velocity: [nil, "0"]).where("velocity NOT LIKE '%.%'")`. This will return all tracked (not imported) points.
3. Run
```ruby
points.update_all("velocity = CAST(ROUND(CAST((CAST(velocity AS FLOAT) * 1000 / 3600) AS NUMERIC), 1) AS TEXT)")
```
This will convert speed in kilometers per hour to meters per second and round it to 1 decimal place.
If you have been using both speed units, but you know the dates where you were tracking with speed in kilometers per hour, on the second step of the instruction above, you can add `where("timestamp BETWEEN ? AND ?", Date.parse("2025-01-01").beginning_of_day.to_i, Date.parse("2025-01-31").end_of_day.to_i)` to the query to convert speed in kilometers per hour to meters per second only for a specific period of time. Resulting query will look like this:
```ruby
start_at = DateTime.new(2025, 1, 1, 0, 0, 0).in_time_zone(Time.current.time_zone).to_i
end_at = DateTime.new(2025, 1, 31, 23, 59, 59).in_time_zone(Time.current.time_zone).to_i
points = Point.where(import_id: nil).where.not(velocity: [nil, "0"]).where("timestamp BETWEEN ? AND ?", start_at, end_at).where("velocity NOT LIKE '%.%'")
```
This will select points tracked between January 1st and January 31st 2025. Then just use step 3 to convert speed in kilometers per hour to meters per second.
### Changed
- Speed for points, that are sent to Dawarich via `POST /api/v1/owntracks/points` endpoint, will now be converted to meters per second, if `topic` param is sent. The official GPSLogger instructions are assuming user won't be sending `topic` param, so this shouldn't affect you if you're using GPSLogger.
### Fixed
- After deleting one point from the map, other points can now be deleted as well. #723 #678
- Fixed a bug where export file was not being deleted from the server after it was deleted. #808
- After an area was drawn on the map, a popup is now being shown to allow user to provide a name and save the area. #740
- Docker entrypoints now use database name to fix problem with custom database names.
- Garmin GPX files with empty tracks are now being imported correctly. #827
### Added
@ -287,7 +330,7 @@ To mount a custom `postgresql.conf` file, you need to create a `postgresql.conf`
```diff
dawarich_db:
image: postgres:14.2-alpine
image: postgis/postgis:14-3.5-alpine
shm_size: 1G
container_name: dawarich_db
volumes:
@ -318,7 +361,7 @@ An example of a custom `postgresql.conf` file is provided in the `postgresql.con
```diff
...
dawarich_db:
image: postgres:14.2-alpine
image: postgis/postgis:14-3.5-alpine
+ shm_size: 1G
...
```
@ -1259,7 +1302,7 @@ deploy:
- shared_data:/var/shared/redis
+ restart: always
dawarich_db:
image: postgres:14.2-alpine
image: postgis/postgis:14-3.5-alpine
container_name: dawarich_db
volumes:
- db_data:/var/lib/postgresql/data

View file

@ -23,7 +23,11 @@ class ExportsController < ApplicationController
end
def destroy
@export.destroy
ActiveRecord::Base.transaction do
@export.destroy
File.delete(Rails.root.join('public', 'exports', @export.name))
end
redirect_to exports_url, notice: 'Export was successfully destroyed.', status: :see_other
end

View file

@ -13,8 +13,7 @@ import {
getSpeedColor
} from "../maps/polylines";
import { fetchAndDrawAreas } from "../maps/areas";
import { handleAreaCreated } from "../maps/areas";
import { fetchAndDrawAreas, handleAreaCreated } from "../maps/areas";
import { showFlashMessage, fetchAndDisplayPhotos, debounce } from "../maps/helpers";
@ -67,7 +66,7 @@ export default class extends Controller {
imperial: this.distanceUnit === 'mi',
metric: this.distanceUnit === 'km',
maxWidth: 120
}).addTo(this.map)
}).addTo(this.map);
// Add stats control
const StatsControl = L.Control.extend({
@ -107,7 +106,13 @@ export default class extends Controller {
// Create a proper Leaflet layer for fog
this.fogOverlay = createFogOverlay();
this.areasLayer = L.layerGroup(); // Initialize areas layer
// Create custom pane for areas
this.map.createPane('areasPane');
this.map.getPane('areasPane').style.zIndex = 650;
this.map.getPane('areasPane').style.pointerEvents = 'all';
// Initialize areasLayer as a feature group and add it to the map immediately
this.areasLayer = new L.FeatureGroup();
this.photoMarkers = L.layerGroup();
this.setupScratchLayer(this.countryCodesMap);
@ -248,10 +253,13 @@ export default class extends Controller {
}
// Store panel state before disconnecting
if (this.rightPanel) {
const finalState = document.querySelector('.leaflet-right-panel').style.display !== 'none' ? 'true' : 'false';
const panel = document.querySelector('.leaflet-right-panel');
const finalState = panel ? (panel.style.display !== 'none' ? 'true' : 'false') : 'false';
localStorage.setItem('mapPanelOpen', finalState);
}
this.map.remove();
if (this.map) {
this.map.remove();
}
}
setupSubscription() {
@ -565,18 +573,23 @@ export default class extends Controller {
fillOpacity: 0.5,
},
},
},
}
});
// Handle circle creation
this.map.on(L.Draw.Event.CREATED, (event) => {
this.map.on('draw:created', (event) => {
const layer = event.layer;
if (event.layerType === 'circle') {
handleAreaCreated(this.areasLayer, layer, this.apiKey);
try {
// Add the layer to the map first
layer.addTo(this.map);
handleAreaCreated(this.areasLayer, layer, this.apiKey);
} catch (error) {
console.error("Error in handleAreaCreated:", error);
console.error(error.stack); // Add stack trace
}
}
this.drawnItems.addLayer(layer);
});
}

View file

@ -1,49 +1,83 @@
import { showFlashMessage } from "./helpers";
export function handleAreaCreated(areasLayer, layer, apiKey) {
const radius = layer.getRadius();
const center = layer.getLatLng();
const formHtml = `
<div class="card w-96 max-w-sm bg-content-100 shadow-xl">
<div class="card w-96">
<div class="card-body">
<h2 class="card-title">New Area</h2>
<form id="circle-form">
<form id="circle-form" class="space-y-4">
<div class="form-control">
<label for="circle-name" class="label">
<span class="label-text">Name</span>
</label>
<input type="text" id="circle-name" name="area[name]" class="input input-bordered input-ghost focus:input-ghost w-full max-w-xs" required>
<input type="text"
id="circle-name"
name="area[name]"
class="input input-bordered w-full"
placeholder="Enter area name"
autofocus
required>
</div>
<input type="hidden" name="area[latitude]" value="${center.lat}">
<input type="hidden" name="area[longitude]" value="${center.lng}">
<input type="hidden" name="area[radius]" value="${radius}">
<div class="card-actions justify-end mt-4">
<button type="submit" class="btn btn-primary">Save</button>
<div class="flex justify-between mt-4">
<button type="button"
class="btn btn-outline"
onclick="this.closest('.leaflet-popup').querySelector('.leaflet-popup-close-button').click()">
Cancel
</button>
<button type="button" id="save-area-btn" class="btn btn-primary">Save Area</button>
</div>
</form>
</div>
</div>
`;
layer.bindPopup(
formHtml, {
maxWidth: "auto",
minWidth: 300
}
).openPopup();
layer.bindPopup(formHtml, {
maxWidth: "auto",
minWidth: 300,
closeButton: true,
closeOnClick: false,
className: 'area-form-popup'
}).openPopup();
layer.on('popupopen', () => {
const form = document.getElementById('circle-form');
if (!form) return;
form.addEventListener('submit', (e) => {
e.preventDefault();
saveArea(new FormData(form), areasLayer, layer, apiKey);
});
});
// Add the layer to the areas layer group
areasLayer.addLayer(layer);
// Bind the event handler immediately after opening the popup
setTimeout(() => {
const form = document.getElementById('circle-form');
const saveButton = document.getElementById('save-area-btn');
const nameInput = document.getElementById('circle-name');
if (!form || !saveButton || !nameInput) {
console.error('Required elements not found');
return;
}
// Focus the name input
nameInput.focus();
// Remove any existing click handlers
const newSaveButton = saveButton.cloneNode(true);
saveButton.parentNode.replaceChild(newSaveButton, saveButton);
// Add click handler
newSaveButton.addEventListener('click', (e) => {
console.log('Save button clicked');
e.preventDefault();
e.stopPropagation();
if (!nameInput.value.trim()) {
nameInput.classList.add('input-error');
return;
}
const formData = new FormData(form);
saveArea(formData, areasLayer, layer, apiKey);
});
}, 100); // Small delay to ensure DOM is ready
}
export function saveArea(formData, areasLayer, layer, apiKey) {
@ -79,9 +113,13 @@ export function saveArea(formData, areasLayer, layer, apiKey) {
// Add event listener for the delete button
layer.on('popupopen', () => {
document.querySelector('.delete-area').addEventListener('click', () => {
deleteArea(data.id, areasLayer, layer, apiKey);
});
const deleteButton = document.querySelector('.delete-area');
if (deleteButton) {
deleteButton.addEventListener('click', (e) => {
e.preventDefault();
deleteArea(data.id, areasLayer, layer, apiKey);
});
}
});
})
.catch(error => {
@ -104,6 +142,8 @@ export function deleteArea(id, areasLayer, layer, apiKey) {
})
.then(data => {
areasLayer.removeLayer(layer); // Remove the layer from the areas layer group
showFlashMessage('notice', `Area was successfully deleted!`);
})
.catch(error => {
console.error('There was a problem with the delete request:', error);
@ -124,33 +164,91 @@ export function fetchAndDrawAreas(areasLayer, apiKey) {
return response.json();
})
.then(data => {
// Clear existing areas
areasLayer.clearLayers();
data.forEach(area => {
// Check if necessary fields are present
if (area.latitude && area.longitude && area.radius && area.name && area.id) {
const layer = L.circle([area.latitude, area.longitude], {
radius: area.radius,
// Convert string coordinates to numbers
const lat = parseFloat(area.latitude);
const lng = parseFloat(area.longitude);
const radius = parseFloat(area.radius);
// Create circle with custom pane
const circle = L.circle([lat, lng], {
radius: radius,
color: 'red',
fillColor: '#f03',
fillOpacity: 0.5
}).bindPopup(`
Name: ${area.name}<br>
Radius: ${Math.round(area.radius)} meters<br>
<a href="#" data-id="${area.id}" class="delete-area">[Delete]</a>
`);
areasLayer.addLayer(layer); // Add to areas layer group
// Add event listener for the delete button
layer.on('popupopen', () => {
document.querySelector('.delete-area').addEventListener('click', (e) => {
e.preventDefault();
if (confirm('Are you sure you want to delete this area?')) {
deleteArea(area.id, areasLayer, layer, apiKey);
}
});
fillOpacity: 0.5,
weight: 2,
interactive: true,
bubblingMouseEvents: false,
pane: 'areasPane'
});
} else {
console.error('Area missing required fields:', area);
// Bind popup content
const popupContent = `
<div class="card w-full">
<div class="card-body">
<h2 class="card-title">${area.name}</h2>
<p>Radius: ${Math.round(radius)} meters</p>
<p>Center: [${lat.toFixed(4)}, ${lng.toFixed(4)}]</p>
<div class="flex justify-end mt-4">
<button class="btn btn-sm btn-error delete-area" data-id="${area.id}">Delete</button>
</div>
</div>
</div>
`;
circle.bindPopup(popupContent);
// Add delete button handler when popup opens
circle.on('popupopen', () => {
const deleteButton = document.querySelector('.delete-area[data-id="' + area.id + '"]');
if (deleteButton) {
deleteButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
if (confirm('Are you sure you want to delete this area?')) {
deleteArea(area.id, areasLayer, circle, apiKey);
}
});
}
});
// Add to layer group
areasLayer.addLayer(circle);
// Wait for the circle to be added to the DOM
setTimeout(() => {
const circlePath = circle.getElement();
if (circlePath) {
// Add CSS styles
circlePath.style.cursor = 'pointer';
circlePath.style.transition = 'all 0.3s ease';
// Add direct DOM event listeners
circlePath.addEventListener('click', (e) => {
e.stopPropagation();
circle.openPopup();
});
circlePath.addEventListener('mouseenter', (e) => {
e.stopPropagation();
circle.setStyle({
fillOpacity: 0.8,
weight: 3
});
});
circlePath.addEventListener('mouseleave', (e) => {
e.stopPropagation();
circle.setStyle({
fillOpacity: 0.5,
weight: 2
});
});
}
}, 100);
}
});
})

View file

@ -144,7 +144,7 @@ class GoogleMaps::PhoneTakeoutParser
end
def parse_raw_array(raw_data)
raw_data.map do |data_point|
raw_data.flat_map do |data_point|
if data_point.dig('visit', 'topCandidate', 'placeLocation')
parse_visit_place_location(data_point)
elsif data_point.dig('activity', 'start') && data_point.dig('activity', 'end')
@ -152,7 +152,7 @@ class GoogleMaps::PhoneTakeoutParser
elsif data_point['timelinePath']
parse_timeline_path(data_point)
end
end.flatten.compact
end.compact
end
def parse_semantic_segments(semantic_segments)

View file

@ -28,7 +28,7 @@ class Gpx::TrackParser
segments = track['trkseg']
segments_array = segments.is_a?(Array) ? segments : [segments]
segments_array.map { |segment| segment['trkpt'] }
segments_array.compact.map { |segment| segment['trkpt'] }
end
def create_point(point, index)

View file

@ -16,7 +16,7 @@ class OwnTracks::Params
altitude: params[:alt],
accuracy: params[:acc],
vertical_accuracy: params[:vac],
velocity: params[:vel],
velocity: speed,
ssid: params[:SSID],
bssid: params[:BSSID],
tracker_id: params[:tid],
@ -69,4 +69,16 @@ class OwnTracks::Params
else 'unknown'
end
end
def speed
return params[:vel] unless owntracks_point?
# OwnTracks speed is in km/h, so we need to convert it to m/s
# Reference: https://owntracks.org/booklet/tech/json/
((params[:vel].to_f * 1000) / 3600).round(1).to_s
end
def owntracks_point?
params[:topic].present?
end
end

View file

@ -1,4 +1,4 @@
FROM ruby:3.3.4-alpine
FROM ruby:3.4.1-alpine
ENV APP_PATH=/var/app
ENV BUNDLE_VERSION=2.5.21

View file

@ -17,7 +17,7 @@ services:
start_period: 30s
timeout: 10s
dawarich_db:
image: postgres:17-alpine
image: postgres:17-alpine # TODO: Use postgis here
shm_size: 1G
container_name: dawarich_db
volumes:

View file

@ -1,159 +0,0 @@
networks:
dawarich:
volumes:
dawarich_public:
name: dawarich_public
dawarich_keydb:
name: dawarich_keydb
dawarich_shared:
name: dawarich_shared
watched:
name: dawarich_watched
services:
app:
container_name: dawarich_app
image: freikin/dawarich:latest
restart: unless-stopped
depends_on:
db:
condition: service_healthy
restart: true
keydb:
condition: service_healthy
restart: true
networks:
- dawarich
ports:
- 3000:3000
environment:
TIME_ZONE: Europe/London
RAILS_ENV: development
REDIS_URL: redis://keydb:6379/0
DATABASE_HOST: db
DATABASE_USERNAME: postgres
DATABASE_PASSWORD: password
DATABASE_NAME: dawarich_development
MIN_MINUTES_SPENT_IN_CITY: 60
APPLICATION_HOSTS: localhost
APPLICATION_PROTOCOL: http
DISTANCE_UNIT: km
stdin_open: true
tty: true
entrypoint: dev-entrypoint.sh
command: [ 'bin/dev' ]
volumes:
- dawarich_public:/var/app/dawarich_public
- watched:/var/app/tmp/imports/watched
healthcheck:
test: [ "CMD-SHELL", "wget -qO - http://127.0.0.1:3000/api/v1/health | grep -q '\"status\"\\s*:\\s*\"ok\"'" ]
start_period: 60s
interval: 15s
timeout: 5s
retries: 3
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "5"
deploy:
resources:
limits:
cpus: '0.50' # Limit CPU usage to 50% of one core
memory: '2G' # Limit memory usage to 2GB
sidekiq:
container_name: dawarich_sidekiq
hostname: sidekiq
image: freikin/dawarich:latest
restart: unless-stopped
depends_on:
app:
condition: service_healthy
restart: true
db:
condition: service_healthy
restart: true
keydb:
condition: service_healthy
restart: true
networks:
- dawarich
environment:
RAILS_ENV: development
REDIS_URL: redis://keydb:6379/0
DATABASE_HOST: db
DATABASE_USERNAME: postgres
DATABASE_PASSWORD: password
DATABASE_NAME: dawarich_development
APPLICATION_HOSTS: localhost
BACKGROUND_PROCESSING_CONCURRENCY: 10
APPLICATION_PROTOCOL: http
DISTANCE_UNIT: km
stdin_open: true
tty: true
entrypoint: dev-entrypoint.sh
command: [ 'sidekiq' ]
volumes:
- dawarich_public:/var/app/dawarich_public
- watched:/var/app/tmp/imports/watched
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "5"
healthcheck:
test: [ "CMD-SHELL", "bundle exec sidekiqmon processes | grep $${HOSTNAME}" ]
interval: 10s
retries: 5
start_period: 30s
timeout: 10s
deploy:
resources:
limits:
cpus: '0.50' # Limit CPU usage to 50% of one core
memory: '2G' # Limit memory usage to 2GB
keydb:
container_name: dawarich-keydb
image: eqalpha/keydb:x86_64_v6.3.4
restart: unless-stopped
networks:
- dawarich
environment:
- TZ=Europe/London
- PUID=1000
- PGID=1000
command: keydb-server /etc/keydb/keydb.conf --appendonly yes --server-threads 4 --active-replica no
volumes:
- dawarich_keydb:/data
- dawarich_shared:/var/shared/redis
healthcheck:
test: [ "CMD", "keydb-cli", "ping" ]
start_period: 60s
interval: 15s
timeout: 5s
retries: 3
db:
container_name: dawarich-db
hostname: db
image: postgres:16.4-alpine3.20
restart: unless-stopped
networks:
- dawarich
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DATABASE: dawarich
volumes:
- ./db:/var/lib/postgresql/data
- dawarich_shared:/var/shared
healthcheck:
test: [ "CMD-SHELL", "pg_isready -q -d $${POSTGRES_DATABASE} -U $${POSTGRES_USER} -h localhost" ]
start_period: 60s
interval: 15s
timeout: 5s
retries: 3

View file

@ -24,7 +24,7 @@ fi
# Wait for the database to become available
echo "⏳ Waiting for database to be ready..."
until PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -c '\q'; do
until PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -d "$DATABASE_NAME" -c '\q'; do
>&2 echo "Postgres is unavailable - retrying..."
sleep 2
done

View file

@ -29,14 +29,14 @@ rm -f $APP_PATH/tmp/pids/server.pid
# Wait for the database to become available
echo "⏳ Waiting for database to be ready..."
until PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -c '\q'; do
until PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -d "$DATABASE_NAME" -c '\q'; do
>&2 echo "Postgres is unavailable - retrying..."
sleep 2
done
echo "✅ PostgreSQL is ready!"
# Create database if it doesn't exist
if ! PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -c "SELECT 1 FROM pg_database WHERE datname='$DATABASE_NAME'" | grep -q 1; then
if ! PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -d "$DATABASE_NAME" -c "SELECT 1 FROM pg_database WHERE datname='$DATABASE_NAME'" | grep -q 1; then
echo "Creating database $DATABASE_NAME..."
bundle exec rails db:create
fi

View file

@ -10,7 +10,7 @@ services:
- ./redis:/var/shared/redis
dawarich_db:
image: postgres:14.2-alpine
image: postgis/postgis:14-3.5-alpine
container_name: dawarich_db
restart: unless-stopped
environment:

View file

@ -27,5 +27,6 @@
<pdop>8.8</pdop>
</trkpt>
</trkseg>
<trkseg></trkseg>
</trk>
</gpx>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
2024-03-01T09:03:09Z * {"bs":2,"p":100.266,"batt":94,"_type":"location","tid":"RO","topic":"owntracks/test/iPhone 12 Pro","alt":36,"lon":13.332,"vel":0,"t":"p","BSSID":"b0:f2:8:45:94:33","SSID":"Home Wifi","conn":"w","vac":4,"acc":10,"tst":1709283789,"lat":52.225,"m":1,"inrids":["5f1d1b"],"inregions":["home"],"_http":true}
2024-03-01T17:46:02Z * {"bs":1,"p":100.28,"batt":94,"_type":"location","tid":"RO","topic":"owntracks/test/iPhone 12 Pro","alt":36,"lon":13.333,"t":"p","vel":0,"BSSID":"b0:f2:8:45:94:33","conn":"w","SSID":"Home Wifi","vac":3,"cog":98,"acc":9,"tst":1709315162,"lat":52.226,"m":1,"inrids":["5f1d1b"],"inregions":["home"],"_http":true}
2024-03-01T09:03:09Z * {"bs":2,"p":100.266,"batt":94,"_type":"location","tid":"RO","topic":"owntracks/test/iPhone 12 Pro","alt":36,"lon":13.332,"vel":5,"t":"p","BSSID":"b0:f2:8:45:94:33","SSID":"Home Wifi","conn":"w","vac":4,"acc":10,"tst":1709283789,"lat":52.225,"m":1,"inrids":["5f1d1b"],"inregions":["home"],"_http":true}
2024-03-01T17:46:02Z * {"bs":1,"p":100.28,"batt":94,"_type":"location","tid":"RO","topic":"owntracks/test/iPhone 12 Pro","alt":36,"lon":13.333,"t":"p","vel":5,"BSSID":"b0:f2:8:45:94:33","conn":"w","SSID":"Home Wifi","vac":3,"cog":98,"acc":9,"tst":1709315162,"lat":52.226,"m":1,"inrids":["5f1d1b"],"inregions":["home"],"_http":true}
2024-03-01T18:26:55Z * {"lon":13.334,"acc":5,"wtst":1696359532,"event":"leave","rid":"5f1d1b","desc":"home","topic":"owntracks/test/iPhone 12 Pro/event","lat":52.227,"t":"c","tst":1709317615,"tid":"RO","_type":"transition","_http":true}
2024-03-01T18:26:55Z * {"cog":40,"batt":85,"lon":13.335,"acc":5,"bs":1,"p":100.279,"vel":3,"vac":3,"lat":52.228,"topic":"owntracks/test/iPhone 12 Pro","t":"c","conn":"m","m":1,"tst":1709317615,"alt":36,"_type":"location","tid":"RO","_http":true}
2024-03-01T18:28:30Z * {"cog":38,"batt":85,"lon":13.336,"acc":5,"bs":1,"p":100.349,"vel":3,"vac":3,"lat":52.229,"topic":"owntracks/test/iPhone 12 Pro","t":"v","conn":"m","m":1,"tst":1709317710,"alt":35,"_type":"location","tid":"RO","_http":true}

View file

@ -76,9 +76,25 @@ RSpec.describe '/exports', type: :request do
end
describe 'DELETE /destroy' do
let!(:export) { create(:export, user:, url: 'exports/export.json') }
let!(:export) { create(:export, user:, url: 'exports/export.json', name: 'export.json') }
let(:export_file) { Rails.root.join('public', 'exports', export.name) }
before { sign_in user }
before do
sign_in user
FileUtils.mkdir_p(File.dirname(export_file))
File.write(export_file, '{"some": "data"}')
end
after { FileUtils.rm_f(export_file) }
it 'removes the export file from disk' do
expect(File.exist?(export_file)).to be true
delete export_url(export)
expect(File.exist?(export_file)).to be false
end
it 'destroys the requested export' do
expect { delete export_url(export) }.to change(Export, :count).by(-1)
@ -89,14 +105,5 @@ RSpec.describe '/exports', type: :request do
expect(response).to redirect_to(exports_url)
end
it 'remove the export file from the disk' do
export_file = Rails.root.join('public', export.url)
FileUtils.touch(export_file)
delete export_url(export)
expect(File.exist?(export_file)).to be_falsey
end
end
end

View file

@ -13,11 +13,11 @@ RSpec.describe Gpx::TrackParser do
context 'when file has a single segment' do
it 'creates points' do
expect { parser }.to change { Point.count }.by(301)
expect { parser }.to change { Point.count }.by(10)
end
it 'broadcasts importing progress' do
expect_any_instance_of(Imports::Broadcaster).to receive(:broadcast_import_progress).exactly(301).times
expect_any_instance_of(Imports::Broadcaster).to receive(:broadcast_import_progress).exactly(10).times
parser
end
@ -27,11 +27,11 @@ RSpec.describe Gpx::TrackParser 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)
expect { parser }.to change { Point.count }.by(43)
end
it 'broadcasts importing progress' do
expect_any_instance_of(Imports::Broadcaster).to receive(:broadcast_import_progress).exactly(558).times
expect_any_instance_of(Imports::Broadcaster).to receive(:broadcast_import_progress).exactly(43).times
parser
end
@ -41,11 +41,11 @@ RSpec.describe Gpx::TrackParser do
let(:file_path) { Rails.root.join('spec/fixtures/files/gpx/gpx_track_multiple_tracks.gpx') }
it 'creates points' do
expect { parser }.to change { Point.count }.by(407)
expect { parser }.to change { Point.count }.by(34)
end
it 'broadcasts importing progress' do
expect_any_instance_of(Imports::Broadcaster).to receive(:broadcast_import_progress).exactly(407).times
expect_any_instance_of(Imports::Broadcaster).to receive(:broadcast_import_progress).exactly(34).times
parser
end

View file

@ -26,7 +26,7 @@ RSpec.describe OwnTracks::ExportParser do
'altitude' => 36,
'accuracy' => 10,
'vertical_accuracy' => 4,
'velocity' => '0',
'velocity' => '1.4',
'connection' => 'wifi',
'ssid' => 'Home Wifi',
'bssid' => 'b0:f2:8:45:94:33',
@ -51,7 +51,7 @@ RSpec.describe OwnTracks::ExportParser do
'tid' => 'RO',
'tst' => 1_709_283_789,
'vac' => 4,
'vel' => 0,
'vel' => 5,
'SSID' => 'Home Wifi',
'batt' => 94,
'conn' => 'w',
@ -64,6 +64,12 @@ RSpec.describe OwnTracks::ExportParser do
}
)
end
it 'correctly converts speed' do
parser
expect(Point.first.velocity).to eq('1.4')
end
end
end
end

View file

@ -20,7 +20,7 @@ RSpec.describe OwnTracks::Params do
altitude: 36,
accuracy: 10,
vertical_accuracy: 4,
velocity: 0,
velocity: '1.4',
ssid: 'Home Wifi',
bssid: 'b0:f2:8:45:94:33',
tracker_id: 'RO',
@ -39,7 +39,7 @@ RSpec.describe OwnTracks::Params do
'topic' => 'owntracks/test/iPhone 12 Pro',
'alt' => 36,
'lon' => 13.332,
'vel' => 0,
'vel' => 5,
't' => 'p',
'BSSID' => 'b0:f2:8:45:94:33',
'SSID' => 'Home Wifi',

View file

@ -16,10 +16,26 @@ describe 'Areas API', type: :request do
parameter name: :area, in: :body, schema: {
type: :object,
properties: {
name: { type: :string },
latitude: { type: :number },
longitude: { type: :number },
radius: { type: :number }
name: {
type: :string,
example: 'Home',
description: 'The name of the area'
},
latitude: {
type: :number,
example: 40.7128,
description: 'The latitude of the area'
},
longitude: {
type: :number,
example: -74.0060,
description: 'The longitude of the area'
},
radius: {
type: :number,
example: 100,
description: 'The radius of the area in meters'
}
},
required: %w[name latitude longitude radius]
}
@ -47,11 +63,31 @@ describe 'Areas API', type: :request do
items: {
type: :object,
properties: {
id: { type: :integer },
name: { type: :string },
latitude: { type: :number },
longitude: { type: :number },
radius: { type: :number }
id: {
type: :integer,
example: 1,
description: 'The ID of the area'
},
name: {
type: :string,
example: 'Home',
description: 'The name of the area'
},
latitude: {
type: :number,
example: 40.7128,
description: 'The latitude of the area'
},
longitude: {
type: :number,
example: -74.0060,
description: 'The longitude of the area'
},
radius: {
type: :number,
example: 100,
description: 'The radius of the area in meters'
}
},
required: %w[id name latitude longitude radius]
}

View file

@ -9,7 +9,12 @@ RSpec.describe 'Api::V1::Countries::VisitedCities', type: :request do
description 'Returns a list of visited cities and countries based on tracked points within the specified date range'
produces 'application/json'
parameter name: :api_key, in: :query, type: :string, required: true
parameter name: :api_key,
in: :query,
type: :string,
required: true,
example: 'a1b2c3d4e5f6g7h8i9j0',
description: 'Your API authentication key'
parameter name: :start_at,
in: :query,
type: :string,
@ -32,6 +37,36 @@ RSpec.describe 'Api::V1::Countries::VisitedCities', type: :request do
data: {
type: :array,
description: 'Array of countries and their visited cities',
example: [
{
country: 'Germany',
cities: [
{
city: 'Berlin',
points: 4394,
timestamp: 1_724_868_369,
stayed_for: 24_490
},
{
city: 'Munich',
points: 2156,
timestamp: 1_724_782_369,
stayed_for: 12_450
}
]
},
{
country: 'France',
cities: [
{
city: 'Paris',
points: 3267,
timestamp: 1_724_695_969,
stayed_for: 18_720
}
]
}
],
items: {
type: :object,
properties: {

View file

@ -8,6 +8,22 @@ describe 'Health API', type: :request do
tags 'Health'
produces 'application/json'
response '200', 'Healthy' do
schema type: :object,
properties: {
status: { type: :string }
}
header 'X-Dawarich-Response',
type: :string,
required: true,
example: 'Hey, I\'m alive!',
description: "Depending on the authentication status of the request, the response will be different. If the request is authenticated, the response will be 'Hey, I'm alive and authenticated!'. If the request is not authenticated, the response will be 'Hey, I'm alive!'."
header 'X-Dawarich-Version',
type: :string,
required: true,
example: '1.0.0',
description: 'The version of the application, for example: 1.0.0'
run_test!
end
end

View file

@ -26,7 +26,8 @@ describe 'Overland Batches API', type: :request do
deferred: 0,
significant_change: 'unknown',
locations_in_payload: 1,
device_id: 'Swagger',
device_id: 'iOS device #166',
unique_id: '1234567890',
wifi: 'unknown',
battery_state: 'unknown',
battery_level: 0
@ -39,36 +40,100 @@ describe 'Overland Batches API', type: :request do
parameter name: :locations, in: :body, schema: {
type: :object,
properties: {
type: { type: :string },
type: { type: :string, example: 'Feature' },
geometry: {
type: :object,
properties: {
type: { type: :string },
coordinates: { type: :array }
type: { type: :string, example: 'Point' },
coordinates: { type: :array, example: [13.356718, 52.502397] }
}
},
properties: {
type: :object,
properties: {
timestamp: { type: :string },
altitude: { type: :number },
speed: { type: :number },
horizontal_accuracy: { type: :number },
vertical_accuracy: { type: :number },
motion: { type: :array },
pauses: { type: :boolean },
activity: { type: :string },
desired_accuracy: { type: :number },
deferred: { type: :number },
significant_change: { type: :string },
locations_in_payload: { type: :number },
device_id: { type: :string },
wifi: { type: :string },
battery_state: { type: :string },
battery_level: { type: :number }
}
},
required: %w[geometry properties]
timestamp: {
type: :string,
example: '2021-06-01T12:00:00Z',
description: 'Timestamp in ISO 8601 format'
},
altitude: {
type: :number,
example: 0,
description: 'Altitude in meters'
},
speed: {
type: :number,
example: 0,
description: 'Speed in meters per second'
},
horizontal_accuracy: {
type: :number,
example: 0,
description: 'Horizontal accuracy in meters'
},
vertical_accuracy: {
type: :number,
example: 0,
description: 'Vertical accuracy in meters'
},
motion: {
type: :array,
example: %w[walking running driving cycling stationary],
description: 'Motion type, for example: automotive_navigation, fitness, other_navigation or other'
},
activity: {
type: :string,
example: 'unknown',
description: 'Activity type, for example: automotive_navigation, fitness, other_navigation or other'
},
desired_accuracy: {
type: :number,
example: 0,
description: 'Desired accuracy in meters'
},
deferred: {
type: :number,
example: 0,
description: 'the distance in meters to defer location updates'
},
significant_change: {
type: :string,
example: 'disabled',
description: 'a significant change mode, disabled, enabled or exclusive'
},
locations_in_payload: {
type: :number,
example: 1,
description: 'the number of locations in the payload'
},
device_id: {
type: :string,
example: 'iOS device #166',
description: 'the device id'
},
unique_id: {
type: :string,
example: '1234567890',
description: 'the device\'s Unique ID as set by Apple'
},
wifi: {
type: :string,
example: 'unknown',
description: 'the WiFi network name'
},
battery_state: {
type: :string,
example: 'unknown',
description: 'the battery state, unknown, unplugged, charging or full'
},
battery_level: {
type: :number,
example: 0,
description: 'the battery level percentage, from 0 to 1'
}
},
required: %w[geometry properties]
}
}
}

View file

@ -39,29 +39,29 @@ describe 'OwnTracks Points API', type: :request do
parameter name: :point, in: :body, schema: {
type: :object,
properties: {
batt: { type: :number },
lon: { type: :number },
acc: { type: :number },
bs: { type: :number },
inrids: { type: :array },
BSSID: { type: :string },
SSID: { type: :string },
vac: { type: :number },
inregions: { type: :array },
lat: { type: :number },
topic: { type: :string },
t: { type: :string },
conn: { type: :string },
m: { type: :number },
tst: { type: :number },
alt: { type: :number },
_type: { type: :string },
tid: { type: :string },
_http: { type: :boolean },
ghash: { type: :string },
isorcv: { type: :string },
isotst: { type: :string },
disptst: { type: :string }
batt: { type: :number, description: 'Device battery level (percentage)' },
lon: { type: :number, description: 'Longitude coordinate' },
acc: { type: :number, description: 'Accuracy of position in meters' },
bs: { type: :number, description: 'Battery status (0=unknown, 1=unplugged, 2=charging, 3=full)' },
inrids: { type: :array, description: 'Array of region IDs device is currently in' },
BSSID: { type: :string, description: 'Connected WiFi access point MAC address' },
SSID: { type: :string, description: 'Connected WiFi network name' },
vac: { type: :number, description: 'Vertical accuracy in meters' },
inregions: { type: :array, description: 'Array of region names device is currently in' },
lat: { type: :number, description: 'Latitude coordinate' },
topic: { type: :string, description: 'MQTT topic in format owntracks/user/device' },
t: { type: :string, description: 'Type of message (p=position, c=circle, etc)' },
conn: { type: :string, description: 'Connection type (w=wifi, m=mobile, o=offline)' },
m: { type: :number, description: 'Motion state (0=stopped, 1=moving)' },
tst: { type: :number, description: 'Timestamp in Unix epoch time' },
alt: { type: :number, description: 'Altitude in meters' },
_type: { type: :string, description: 'Internal message type (usually "location")' },
tid: { type: :string, description: 'Tracker ID used to display the initials of a user' },
_http: { type: :boolean, description: 'Whether message was sent via HTTP (true) or MQTT (false)' },
ghash: { type: :string, description: 'Geohash of location' },
isorcv: { type: :string, description: 'ISO 8601 timestamp when message was received' },
isotst: { type: :string, description: 'ISO 8601 timestamp of the location fix' },
disptst: { type: :string, description: 'Human-readable timestamp of the location fix' }
},
required: %w[owntracks/jane]
}

View file

@ -101,27 +101,73 @@ describe 'Points API', type: :request do
geometry: {
type: :object,
properties: {
type: { type: :string },
coordinates: { type: :array, items: { type: :number } }
type: {
type: :string,
example: 'Point',
description: 'the geometry type, always Point'
},
coordinates: {
type: :array,
items: {
type: :number,
example: [-122.40530871, 37.74430413],
description: 'the coordinates of the point, longitude and latitude'
}
}
}
},
properties: {
type: :object,
properties: {
timestamp: { type: :string },
horizontal_accuracy: { type: :number },
vertical_accuracy: { type: :number },
altitude: { type: :number },
speed: { type: :number },
speed_accuracy: { type: :number },
course: { type: :number },
course_accuracy: { type: :number },
track_id: { type: :string },
device_id: { type: :string }
timestamp: {
type: :string,
example: '2025-01-17T21:03:01Z',
description: 'the timestamp of the point'
},
horizontal_accuracy: {
type: :number,
example: 5,
description: 'the horizontal accuracy of the point in meters'
},
vertical_accuracy: {
type: :number,
example: -1,
description: 'the vertical accuracy of the point in meters'
},
altitude: {
type: :number,
example: 0,
description: 'the altitude of the point in meters'
},
speed: {
type: :number,
example: 92.088,
description: 'the speed of the point in meters per second'
},
speed_accuracy: {
type: :number,
example: 0,
description: 'the speed accuracy of the point in meters per second'
},
course_accuracy: {
type: :number,
example: 0,
description: 'the course accuracy of the point in degrees'
},
track_id: {
type: :string,
example: '799F32F5-89BB-45FB-A639-098B1B95B09F',
description: 'the track id of the point set by the device'
},
device_id: {
type: :string,
example: '8D5D4197-245B-4619-A88B-2049100ADE46',
description: 'the device id of the point set by the device'
}
}
}
},
required: %w[geometry properties]
},
required: %w[geometry properties]
}
}
parameter name: :api_key, in: :query, type: :string, required: true, description: 'API Key'

View file

@ -20,12 +20,26 @@ describe 'Settings API', type: :request do
parameter name: :settings, in: :body, schema: {
type: :object,
properties: {
route_opacity: { type: :number },
meters_between_routes: { type: :number },
minutes_between_routes: { type: :number },
fog_of_war_meters: { type: :number },
time_threshold_minutes: { type: :number },
merge_threshold_minutes: { type: :number }
route_opacity: {
type: :number,
example: 0.3,
description: 'the opacity of the route, float between 0 and 1'
},
meters_between_routes: {
type: :number,
example: 100,
description: 'the distance between routes in meters'
},
minutes_between_routes: {
type: :number,
example: 100,
description: 'the time between routes in minutes'
},
fog_of_war_meters: {
type: :number,
example: 100,
description: 'the fog of war distance in meters'
}
},
optional: %w[route_opacity meters_between_routes minutes_between_routes fog_of_war_meters
time_threshold_minutes merge_threshold_minutes]
@ -49,12 +63,26 @@ describe 'Settings API', type: :request do
settings: {
type: :object,
properties: {
route_opacity: { type: :string },
meters_between_routes: { type: :string },
minutes_between_routes: { type: :string },
fog_of_war_meters: { type: :string },
time_threshold_minutes: { type: :string },
merge_threshold_minutes: { type: :string }
route_opacity: {
type: :string,
example: 0.3,
description: 'the opacity of the route, float between 0 and 1'
},
meters_between_routes: {
type: :string,
example: 100,
description: 'the distance between routes in meters'
},
minutes_between_routes: {
type: :string,
example: 100,
description: 'the time between routes in minutes'
},
fog_of_war_meters: {
type: :string,
example: 100,
description: 'the fog of war distance in meters'
}
},
required: %w[route_opacity meters_between_routes minutes_between_routes fog_of_war_meters
time_threshold_minutes merge_threshold_minutes]

View file

@ -29,12 +29,20 @@ paths:
properties:
name:
type: string
example: Home
description: The name of the area
latitude:
type: number
example: 40.7128
description: The latitude of the area
longitude:
type: number
example: -74.006
description: The longitude of the area
radius:
type: number
example: 100
description: The radius of the area in meters
required:
- name
- latitude
@ -71,14 +79,24 @@ paths:
properties:
id:
type: integer
example: 1
description: The ID of the area
name:
type: string
example: Home
description: The name of the area
latitude:
type: number
example: 40.7128
description: The latitude of the area
longitude:
type: number
example: -74.006
description: The longitude of the area
radius:
type: number
example: 100
description: The radius of the area in meters
required:
- id
- name
@ -117,6 +135,8 @@ paths:
- name: api_key
in: query
required: true
example: a1b2c3d4e5f6g7h8i9j0
description: Your API authentication key
schema:
type: string
- name: start_at
@ -146,6 +166,23 @@ paths:
data:
type: array
description: Array of countries and their visited cities
example:
- country: Germany
cities:
- city: Berlin
points: 4394
timestamp: 1724868369
stayed_for: 24490
- city: Munich
points: 2156
timestamp: 1724782369
stayed_for: 12450
- country: France
cities:
- city: Paris
points: 3267
timestamp: 1724695969
stayed_for: 18720
items:
type: object
properties:
@ -192,6 +229,27 @@ paths:
responses:
'200':
description: Healthy
headers:
X-Dawarich-Response:
type: string
required: true
example: Hey, I'm alive!
description: Depending on the authentication status of the request,
the response will be different. If the request is authenticated, the
response will be 'Hey, I'm alive and authenticated!'. If the request
is not authenticated, the response will be 'Hey, I'm alive!'.
X-Dawarich-Version:
type: string
required: true
example: 1.0.0
description: 'The version of the application, for example: 1.0.0'
content:
application/json:
schema:
type: object
properties:
status:
type: string
"/api/v1/overland/batches":
post:
summary: Creates a batch of points
@ -217,51 +275,97 @@ paths:
properties:
type:
type: string
example: Feature
geometry:
type: object
properties:
type:
type: string
example: Point
coordinates:
type: array
example:
- 13.356718
- 52.502397
properties:
type: object
properties:
timestamp:
type: string
example: '2021-06-01T12:00:00Z'
description: Timestamp in ISO 8601 format
altitude:
type: number
example: 0
description: Altitude in meters
speed:
type: number
example: 0
description: Speed in meters per second
horizontal_accuracy:
type: number
example: 0
description: Horizontal accuracy in meters
vertical_accuracy:
type: number
example: 0
description: Vertical accuracy in meters
motion:
type: array
pauses:
type: boolean
example:
- walking
- running
- driving
- cycling
- stationary
description: 'Motion type, for example: automotive_navigation,
fitness, other_navigation or other'
activity:
type: string
example: unknown
description: 'Activity type, for example: automotive_navigation,
fitness, other_navigation or other'
desired_accuracy:
type: number
example: 0
description: Desired accuracy in meters
deferred:
type: number
example: 0
description: the distance in meters to defer location updates
significant_change:
type: string
example: disabled
description: a significant change mode, disabled, enabled or
exclusive
locations_in_payload:
type: number
example: 1
description: the number of locations in the payload
device_id:
type: string
example: 'iOS device #166'
description: the device id
unique_id:
type: string
example: '1234567890'
description: the device's Unique ID as set by Apple
wifi:
type: string
example: unknown
description: the WiFi network name
battery_state:
type: string
example: unknown
description: the battery state, unknown, unplugged, charging
or full
battery_level:
type: number
required:
- geometry
- properties
example: 0
description: the battery level percentage, from 0 to 1
required:
- geometry
- properties
examples:
'0':
summary: Creates a batch of points
@ -286,7 +390,8 @@ paths:
deferred: 0
significant_change: unknown
locations_in_payload: 1
device_id: Swagger
device_id: 'iOS device #166'
unique_id: '1234567890'
wifi: unknown
battery_state: unknown
battery_level: 0
@ -315,50 +420,74 @@ paths:
properties:
batt:
type: number
description: Device battery level (percentage)
lon:
type: number
description: Longitude coordinate
acc:
type: number
description: Accuracy of position in meters
bs:
type: number
description: Battery status (0=unknown, 1=unplugged, 2=charging,
3=full)
inrids:
type: array
description: Array of region IDs device is currently in
BSSID:
type: string
description: Connected WiFi access point MAC address
SSID:
type: string
description: Connected WiFi network name
vac:
type: number
description: Vertical accuracy in meters
inregions:
type: array
description: Array of region names device is currently in
lat:
type: number
description: Latitude coordinate
topic:
type: string
description: MQTT topic in format owntracks/user/device
t:
type: string
description: Type of message (p=position, c=circle, etc)
conn:
type: string
description: Connection type (w=wifi, m=mobile, o=offline)
m:
type: number
description: Motion state (0=stopped, 1=moving)
tst:
type: number
description: Timestamp in Unix epoch time
alt:
type: number
description: Altitude in meters
_type:
type: string
description: Internal message type (usually "location")
tid:
type: string
description: Tracker ID used to display the initials of a user
_http:
type: boolean
description: Whether message was sent via HTTP (true) or MQTT (false)
ghash:
type: string
description: Geohash of location
isorcv:
type: string
description: ISO 8601 timestamp when message was received
isotst:
type: string
description: ISO 8601 timestamp of the location fix
disptst:
type: string
description: Human-readable timestamp of the location fix
required:
- owntracks/jane
examples:
@ -725,36 +854,58 @@ paths:
properties:
type:
type: string
example: Point
description: the geometry type, always Point
coordinates:
type: array
items:
type: number
example:
- -122.40530871
- 37.74430413
description: the coordinates of the point, longitude and latitude
properties:
type: object
properties:
timestamp:
type: string
example: '2025-01-17T21:03:01Z'
description: the timestamp of the point
horizontal_accuracy:
type: number
example: 5
description: the horizontal accuracy of the point in meters
vertical_accuracy:
type: number
example: -1
description: the vertical accuracy of the point in meters
altitude:
type: number
example: 0
description: the altitude of the point in meters
speed:
type: number
example: 92.088
description: the speed of the point in meters per second
speed_accuracy:
type: number
course:
type: number
example: 0
description: the speed accuracy of the point in meters per second
course_accuracy:
type: number
example: 0
description: the course accuracy of the point in degrees
track_id:
type: string
example: 799F32F5-89BB-45FB-A639-098B1B95B09F
description: the track id of the point set by the device
device_id:
type: string
required:
- geometry
- properties
example: 8D5D4197-245B-4619-A88B-2049100ADE46
description: the device id of the point set by the device
required:
- geometry
- properties
examples:
'0':
summary: Creates a batch of points
@ -821,16 +972,20 @@ paths:
properties:
route_opacity:
type: number
example: 0.3
description: the opacity of the route, float between 0 and 1
meters_between_routes:
type: number
example: 100
description: the distance between routes in meters
minutes_between_routes:
type: number
example: 100
description: the time between routes in minutes
fog_of_war_meters:
type: number
time_threshold_minutes:
type: number
merge_threshold_minutes:
type: number
example: 100
description: the fog of war distance in meters
optional:
- route_opacity
- meters_between_routes
@ -873,16 +1028,21 @@ paths:
properties:
route_opacity:
type: string
example: 0.3
description: the opacity of the route, float between 0 and
1
meters_between_routes:
type: string
example: 100
description: the distance between routes in meters
minutes_between_routes:
type: string
example: 100
description: the time between routes in minutes
fog_of_war_meters:
type: string
time_threshold_minutes:
type: string
merge_threshold_minutes:
type: string
example: 100
description: the fog of war distance in meters
required:
- route_opacity
- meters_between_routes