mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-13 10:41:38 -05:00
commit
0cc5ac0860
15 changed files with 801 additions and 183 deletions
|
|
@ -1 +1 @@
|
|||
0.12.1
|
||||
0.12.2
|
||||
|
|
|
|||
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -5,6 +5,19 @@ 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.12.2] — 2024-08-28
|
||||
|
||||
### Added
|
||||
|
||||
- `PATCH /api/v1/settings` endpoint to update user settings with swagger docs
|
||||
- `GET /api/v1/settings` endpoint to get user settings with swagger docs
|
||||
- Missing `page` and `per_page` query parameters to the `GET /api/v1/points` endpoint swagger docs
|
||||
|
||||
### Changed
|
||||
|
||||
- Map settings moved to the map itself and are available in the top right corner of the map under the gear icon.
|
||||
|
||||
|
||||
## [0.12.1] — 2024-08-25
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -23,3 +23,37 @@
|
|||
.timeline-box {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Style for the settings panel */
|
||||
.leaflet-settings-panel {
|
||||
background-color: white;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.leaflet-settings-panel label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.leaflet-settings-panel input {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
padding: 5px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.leaflet-settings-panel button {
|
||||
padding: 5px 10px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.leaflet-settings-panel button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
|
|
|||
36
app/controllers/api/v1/settings_controller.rb
Normal file
36
app/controllers/api/v1/settings_controller.rb
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::SettingsController < ApiController
|
||||
def index
|
||||
render json: {
|
||||
settings: current_api_user.settings,
|
||||
status: 'success'
|
||||
}, status: :ok
|
||||
end
|
||||
|
||||
def update
|
||||
settings_params.each { |key, value| current_api_user.settings[key] = value }
|
||||
|
||||
if current_api_user.save
|
||||
render json: {
|
||||
message: 'Settings updated',
|
||||
settings: current_api_user.settings,
|
||||
status: 'success'
|
||||
}, status: :ok
|
||||
else
|
||||
render json: {
|
||||
message: 'Something went wrong',
|
||||
errors: current_api_user.errors.full_messages
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def settings_params
|
||||
params.require(:settings).permit(
|
||||
:meters_between_routes, :minutes_between_routes, :fog_of_war_meters,
|
||||
:time_threshold_minutes, :merge_threshold_minutes, :route_opacity
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -13,14 +13,18 @@ import "leaflet-draw";
|
|||
export default class extends Controller {
|
||||
static targets = ["container"];
|
||||
|
||||
settingsButtonAdded = false;
|
||||
layerControl = null;
|
||||
|
||||
connect() {
|
||||
console.log("Map controller connected");
|
||||
|
||||
this.apiKey = this.element.dataset.api_key;
|
||||
this.markers = JSON.parse(this.element.dataset.coordinates);
|
||||
this.timezone = this.element.dataset.timezone;
|
||||
this.clearFogRadius = this.element.dataset.fog_of_war_meters;
|
||||
this.routeOpacity = parseInt(this.element.dataset.route_opacity) / 100 || 0.6;
|
||||
this.userSettings = JSON.parse(this.element.dataset.user_settings);
|
||||
this.clearFogRadius = parseInt(this.userSettings.fog_of_war_meters) || 50;
|
||||
this.routeOpacity = parseFloat(this.userSettings.route_opacity) || 0.6;
|
||||
|
||||
this.center = this.markers[this.markers.length - 1] || [52.514568, 13.350111];
|
||||
|
||||
|
|
@ -35,6 +39,10 @@ export default class extends Controller {
|
|||
this.fogOverlay = L.layerGroup(); // Initialize fog layer
|
||||
this.areasLayer = L.layerGroup(); // Initialize areas layer
|
||||
|
||||
if (!this.settingsButtonAdded) {
|
||||
this.addSettingsButton();
|
||||
}
|
||||
|
||||
const controlsLayer = {
|
||||
Points: this.markersLayer,
|
||||
Polylines: this.polylinesLayer,
|
||||
|
|
@ -52,7 +60,7 @@ export default class extends Controller {
|
|||
})
|
||||
.addTo(this.map);
|
||||
|
||||
L.control.layers(this.baseMaps(), controlsLayer).addTo(this.map);
|
||||
this.layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(this.map);
|
||||
|
||||
// Fetch and draw areas when the map is loaded
|
||||
this.fetchAndDrawAreas(this.apiKey);
|
||||
|
|
@ -318,8 +326,8 @@ export default class extends Controller {
|
|||
createPolylinesLayer(markers, map, timezone, routeOpacity) {
|
||||
const splitPolylines = [];
|
||||
let currentPolyline = [];
|
||||
const distanceThresholdMeters = parseInt(this.element.dataset.meters_between_routes) || 500;
|
||||
const timeThresholdMinutes = parseInt(this.element.dataset.minutes_between_routes) || 60;
|
||||
const distanceThresholdMeters = parseInt(this.userSettings.meters_between_routes) || 500;
|
||||
const timeThresholdMinutes = parseInt(this.userSettings.minutes_between_routes) || 60;
|
||||
|
||||
for (let i = 0, len = markers.length; i < len; i++) {
|
||||
if (currentPolyline.length === 0) {
|
||||
|
|
@ -459,7 +467,6 @@ export default class extends Controller {
|
|||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Circle saved:', data);
|
||||
layer.closePopup();
|
||||
layer.bindPopup(`
|
||||
Name: ${data.name}<br>
|
||||
|
|
@ -515,12 +522,7 @@ export default class extends Controller {
|
|||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Fetched areas:', data); // Debugging line to check response
|
||||
|
||||
data.forEach(area => {
|
||||
// Log each area to verify the structure
|
||||
console.log('Area:', 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], {
|
||||
|
|
@ -535,7 +537,6 @@ export default class extends Controller {
|
|||
`);
|
||||
|
||||
this.areasLayer.addLayer(layer); // Add to areas layer group
|
||||
console.log('Added layer to areasLayer:', layer); // Debugging line to confirm addition
|
||||
|
||||
// Add event listener for the delete button
|
||||
layer.on('popupopen', () => {
|
||||
|
|
@ -555,4 +556,361 @@ export default class extends Controller {
|
|||
console.error('There was a problem with the fetch request:', error);
|
||||
});
|
||||
}
|
||||
|
||||
addSettingsButton() {
|
||||
if (this.settingsButtonAdded) return;
|
||||
|
||||
console.log('Adding settings button');
|
||||
// Define the custom control
|
||||
const SettingsControl = L.Control.extend({
|
||||
onAdd: (map) => {
|
||||
const button = L.DomUtil.create('button', 'map-settings-button');
|
||||
button.innerHTML = '⚙️'; // Gear icon
|
||||
|
||||
// Style the button
|
||||
button.style.backgroundColor = 'white';
|
||||
button.style.width = '32px';
|
||||
button.style.height = '32px';
|
||||
button.style.border = 'none';
|
||||
button.style.cursor = 'pointer';
|
||||
button.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)';
|
||||
|
||||
// Disable map interactions when clicking the button
|
||||
L.DomEvent.disableClickPropagation(button);
|
||||
|
||||
// Toggle settings menu on button click
|
||||
L.DomEvent.on(button, 'click', () => {
|
||||
this.toggleSettingsMenu();
|
||||
});
|
||||
|
||||
return button;
|
||||
}
|
||||
});
|
||||
|
||||
// Add the control to the map
|
||||
this.map.addControl(new SettingsControl({ position: 'topleft' }));
|
||||
this.settingsButtonAdded = true;
|
||||
}
|
||||
|
||||
toggleSettingsMenu() {
|
||||
// If the settings panel already exists, just show/hide it
|
||||
if (this.settingsPanel) {
|
||||
if (this.settingsPanel._map) {
|
||||
this.map.removeControl(this.settingsPanel);
|
||||
} else {
|
||||
this.map.addControl(this.settingsPanel);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the settings panel for the first time
|
||||
this.settingsPanel = L.control({ position: 'topleft' });
|
||||
|
||||
this.settingsPanel.onAdd = () => {
|
||||
const div = L.DomUtil.create('div', 'leaflet-settings-panel');
|
||||
|
||||
// Form HTML
|
||||
div.innerHTML = `
|
||||
<form id="settings-form" class="w-48">
|
||||
<label for="route-opacity">Route Opacity</label>
|
||||
<div class="join">
|
||||
<input type="number" class="input input-ghost join-item focus:input-ghost input-xs input-bordered w-full max-w-xs" id="route-opacity" name="route_opacity" min="0" max="1" step="0.1" value="${this.routeOpacity}">
|
||||
<label for="route_opacity_info" class="btn-xs join-item ">?</label>
|
||||
|
||||
</div>
|
||||
|
||||
<label for="fog_of_war_meters">Fog of War radius</label>
|
||||
<div class="join">
|
||||
<input type="number" class="join-item input input-ghost focus:input-ghost input-xs input-bordered w-full max-w-xs" id="fog_of_war_meters" name="fog_of_war_meters" min="5" max="100" step="1" value="${this.clearFogRadius}">
|
||||
<label for="fog_of_war_meters_info" class="btn-xs join-item">?</label>
|
||||
</div>
|
||||
|
||||
|
||||
<label for="meters_between_routes">Meters between routes</label>
|
||||
<div class="join">
|
||||
<input type="number" class="join-item input input-ghost focus:input-ghost input-xs input-bordered w-full max-w-xs" id="meters_between_routes" name="meters_between_routes" step="1" value="${this.userSettings.meters_between_routes}">
|
||||
<label for="meters_between_routes_info" class="btn-xs join-item">?</label>
|
||||
</div>
|
||||
|
||||
|
||||
<label for="minutes_between_routes">Minutes between routes</label>
|
||||
<div class="join">
|
||||
<input type="number" class="join-item input input-ghost focus:input-ghost input-xs input-bordered w-full max-w-xs" id="minutes_between_routes" name="minutes_between_routes" step="1" value="${this.userSettings.minutes_between_routes}">
|
||||
<label for="minutes_between_routes_info" class="btn-xs join-item">?</label>
|
||||
</div>
|
||||
|
||||
|
||||
<label for="time_threshold_minutes">Time threshold minutes</label>
|
||||
<div class="join">
|
||||
<input type="number" class="join-item input input-ghost focus:input-ghost input-xs input-bordered w-full max-w-xs" id="time_threshold_minutes" name="time_threshold_minutes" step="1" value="${this.userSettings.time_threshold_minutes}">
|
||||
<label for="time_threshold_minutes_info" class="btn-xs join-item">?</label>
|
||||
</div>
|
||||
|
||||
|
||||
<label for="merge_threshold_minutes">Merge threshold minutes</label>
|
||||
<div class="join">
|
||||
<input type="number" class="join-item input input-ghost focus:input-ghost input-xs input-bordered w-full max-w-xs" id="merge_threshold_minutes" name="merge_threshold_minutes" step="1" value="${this.userSettings.merge_threshold_minutes}">
|
||||
<label for="merge_threshold_minutes_info" class="btn-xs join-item">?</label>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<button type="submit">Update</button>
|
||||
</form>
|
||||
`;
|
||||
|
||||
// Style the panel
|
||||
div.style.backgroundColor = 'white';
|
||||
div.style.padding = '10px';
|
||||
div.style.border = '1px solid #ccc';
|
||||
div.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)';
|
||||
|
||||
// Prevent map interactions when interacting with the form
|
||||
L.DomEvent.disableClickPropagation(div);
|
||||
|
||||
// Add event listener to the form submission
|
||||
div.querySelector('#settings-form').addEventListener(
|
||||
'submit', this.updateSettings.bind(this)
|
||||
);
|
||||
|
||||
return div;
|
||||
};
|
||||
|
||||
this.map.addControl(this.settingsPanel);
|
||||
}
|
||||
|
||||
updateSettings(event) {
|
||||
event.preventDefault();
|
||||
|
||||
fetch(`/api/v1/settings?api_key=${this.apiKey}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
settings: {
|
||||
route_opacity: event.target.route_opacity.value,
|
||||
fog_of_war_meters: event.target.fog_of_war_meters.value,
|
||||
meters_between_routes: event.target.meters_between_routes.value,
|
||||
minutes_between_routes: event.target.minutes_between_routes.value,
|
||||
time_threshold_minutes: event.target.time_threshold_minutes.value,
|
||||
merge_threshold_minutes: event.target.merge_threshold_minutes.value,
|
||||
},
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.status === 'success') {
|
||||
this.showFlashMessage('notice', data.message);
|
||||
this.updateMapWithNewSettings(data.settings);
|
||||
} else {
|
||||
this.showFlashMessage('error', data.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showFlashMessage(type, message) {
|
||||
// Create the outer flash container div
|
||||
const flashDiv = document.createElement('div');
|
||||
flashDiv.setAttribute('data-controller', 'removals');
|
||||
flashDiv.className = `flex items-center fixed top-5 right-5 ${this.classesForFlash(type)} py-3 px-5 rounded-lg`;
|
||||
|
||||
// Create the message div
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'mr-4';
|
||||
messageDiv.innerText = message;
|
||||
|
||||
// Create the close button
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.setAttribute('type', 'button');
|
||||
closeButton.setAttribute('data-action', 'click->removals#remove');
|
||||
|
||||
// Create the SVG icon for the close button
|
||||
const closeIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
closeIcon.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
||||
closeIcon.setAttribute('class', 'h-6 w-6');
|
||||
closeIcon.setAttribute('fill', 'none');
|
||||
closeIcon.setAttribute('viewBox', '0 0 24 24');
|
||||
closeIcon.setAttribute('stroke', 'currentColor');
|
||||
|
||||
const closeIconPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
closeIconPath.setAttribute('stroke-linecap', 'round');
|
||||
closeIconPath.setAttribute('stroke-linejoin', 'round');
|
||||
closeIconPath.setAttribute('stroke-width', '2');
|
||||
closeIconPath.setAttribute('d', 'M6 18L18 6M6 6l12 12');
|
||||
|
||||
// Append the path to the SVG
|
||||
closeIcon.appendChild(closeIconPath);
|
||||
// Append the SVG to the close button
|
||||
closeButton.appendChild(closeIcon);
|
||||
|
||||
// Append the message and close button to the flash div
|
||||
flashDiv.appendChild(messageDiv);
|
||||
flashDiv.appendChild(closeButton);
|
||||
|
||||
// Append the flash message to the body or a specific flash container
|
||||
document.body.appendChild(flashDiv);
|
||||
|
||||
// Optional: Automatically remove the flash message after 5 seconds
|
||||
setTimeout(() => {
|
||||
flashDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Helper function to get flash classes based on type
|
||||
classesForFlash(type) {
|
||||
switch (type) {
|
||||
case 'error':
|
||||
return 'bg-red-100 text-red-700 border-red-300';
|
||||
case 'notice':
|
||||
return 'bg-blue-100 text-blue-700 border-blue-300';
|
||||
default:
|
||||
return 'bg-blue-100 text-blue-700 border-blue-300';
|
||||
}
|
||||
}
|
||||
|
||||
updateMapWithNewSettings(newSettings) {
|
||||
const currentLayerStates = this.getLayerControlStates();
|
||||
|
||||
// Update local state with new settings
|
||||
this.clearFogRadius = parseInt(newSettings.fog_of_war_meters) || 50;
|
||||
this.routeOpacity = parseFloat(newSettings.route_opacity) || 0.6;
|
||||
|
||||
// Preserve existing layer instances if they exist
|
||||
const preserveLayers = {
|
||||
Points: this.markersLayer,
|
||||
Polylines: this.polylinesLayer,
|
||||
Heatmap: this.heatmapLayer,
|
||||
"Fog of War": this.fogOverlay,
|
||||
Areas: this.areasLayer,
|
||||
};
|
||||
|
||||
// Clear all layers except base layers
|
||||
this.map.eachLayer((layer) => {
|
||||
if (!(layer instanceof L.TileLayer)) {
|
||||
this.map.removeLayer(layer);
|
||||
}
|
||||
});
|
||||
|
||||
// Recreate layers only if they don't exist
|
||||
this.markersLayer = preserveLayers.Points || L.layerGroup(this.createMarkersArray(this.markers));
|
||||
this.polylinesLayer = preserveLayers.Polylines || this.createPolylinesLayer(this.markers, this.map, this.timezone, this.routeOpacity);
|
||||
this.heatmapLayer = preserveLayers.Heatmap || L.heatLayer(this.markers.map((element) => [element[0], element[1], 0.2]), { radius: 20 });
|
||||
this.fogOverlay = preserveLayers["Fog of War"] || L.layerGroup();
|
||||
this.areasLayer = preserveLayers.Areas || L.layerGroup();
|
||||
|
||||
// Redraw areas
|
||||
this.fetchAndDrawAreas(this.apiKey);
|
||||
|
||||
let fogEnabled = false;
|
||||
document.getElementById('fog').style.display = 'none';
|
||||
|
||||
this.map.on('overlayadd', (e) => {
|
||||
if (e.name === 'Fog of War') {
|
||||
fogEnabled = true;
|
||||
document.getElementById('fog').style.display = 'block';
|
||||
this.updateFog(this.markers, this.clearFogRadius);
|
||||
}
|
||||
});
|
||||
|
||||
this.map.on('overlayremove', (e) => {
|
||||
if (e.name === 'Fog of War') {
|
||||
fogEnabled = false;
|
||||
document.getElementById('fog').style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
this.map.on('zoomend moveend', () => {
|
||||
if (fogEnabled) {
|
||||
this.updateFog(this.markers, this.clearFogRadius);
|
||||
}
|
||||
});
|
||||
|
||||
this.addLastMarker(this.map, this.markers);
|
||||
this.addEventListeners();
|
||||
this.initializeDrawControl();
|
||||
this.updatePolylinesOpacity(this.routeOpacity);
|
||||
|
||||
this.map.on('overlayadd', (e) => {
|
||||
if (e.name === 'Areas') {
|
||||
this.map.addControl(this.drawControl);
|
||||
}
|
||||
});
|
||||
|
||||
this.map.on('overlayremove', (e) => {
|
||||
if (e.name === 'Areas') {
|
||||
this.map.removeControl(this.drawControl);
|
||||
}
|
||||
});
|
||||
|
||||
this.applyLayerControlStates(currentLayerStates);
|
||||
}
|
||||
|
||||
getLayerControlStates() {
|
||||
const controls = {};
|
||||
|
||||
this.map.eachLayer((layer) => {
|
||||
const layerName = this.getLayerName(layer);
|
||||
|
||||
if (layerName) {
|
||||
controls[layerName] = this.map.hasLayer(layer);
|
||||
}
|
||||
});
|
||||
|
||||
return controls;
|
||||
}
|
||||
|
||||
getLayerName(layer) {
|
||||
const controlLayers = {
|
||||
Points: this.markersLayer,
|
||||
Polylines: this.polylinesLayer,
|
||||
Heatmap: this.heatmapLayer,
|
||||
"Fog of War": this.fogOverlay,
|
||||
Areas: this.areasLayer,
|
||||
};
|
||||
|
||||
for (const [name, val] of Object.entries(controlLayers)) {
|
||||
if (val && val.hasLayer && layer && val.hasLayer(layer)) // Check if the group layer contains the current layer
|
||||
return name;
|
||||
}
|
||||
|
||||
// Direct instance matching
|
||||
for (const [name, val] of Object.entries(controlLayers)) {
|
||||
if (val === layer) return name;
|
||||
}
|
||||
|
||||
return undefined; // Indicate no matching layer name found
|
||||
}
|
||||
|
||||
|
||||
applyLayerControlStates(states) {
|
||||
const layerControl = {
|
||||
Points: this.markersLayer,
|
||||
Polylines: this.polylinesLayer,
|
||||
Heatmap: this.heatmapLayer,
|
||||
"Fog of War": this.fogOverlay,
|
||||
Areas: this.areasLayer,
|
||||
};
|
||||
|
||||
for (const [name, isVisible] of Object.entries(states)) {
|
||||
const layer = layerControl[name];
|
||||
|
||||
if (isVisible && !this.map.hasLayer(layer)) {
|
||||
this.map.addLayer(layer);
|
||||
} else if (this.map.hasLayer(layer)) {
|
||||
this.map.removeLayer(layer);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the layer control reflects the current state
|
||||
this.map.removeControl(this.layerControl);
|
||||
this.layerControl = L.control.layers(this.baseMaps(), layerControl).addTo(this.map);
|
||||
}
|
||||
|
||||
updatePolylinesOpacity(opacity) {
|
||||
this.polylinesLayer.eachLayer((layer) => {
|
||||
if (layer instanceof L.Polyline) {
|
||||
layer.setStyle({ opacity: opacity });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
97
app/views/map/_settings_modals.html.erb
Normal file
97
app/views/map/_settings_modals.html.erb
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<!-- Put this part before </body> tag -->
|
||||
<input type="checkbox" id="route_opacity_info" class="modal-toggle" />
|
||||
<div class="modal focus:z-99" role="dialog">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">Route opacity</h3>
|
||||
<p class="py-4">
|
||||
Value in percent.
|
||||
</p>
|
||||
<p class="py-4">
|
||||
This value is the opacity of the route on the map. The value is in percent, and it can be set from 0 to 100. The default value is 100, which means that the route is fully visible. If you set the value to 0, the route will be invisible.
|
||||
</p>
|
||||
</div>
|
||||
<label class="modal-backdrop" for="route_opacity_info">Close</label>
|
||||
</div>
|
||||
|
||||
<input type="checkbox" id="fog_of_war_meters_info" class="modal-toggle" />
|
||||
<div class="modal focus:z-99" role="dialog">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">Fog of War</h3>
|
||||
<p class="py-4">
|
||||
Value in meters.
|
||||
</p>
|
||||
<p class="py-4">
|
||||
Here you can set the radius of the "cleared" area around a point when Fog of War mode is enabled. The area around the point will be cleared, and the rest of the map will be covered with fog. The cleared area will be a circle with the point as the center and the radius as the value you set here.
|
||||
</p>
|
||||
</div>
|
||||
<label class="modal-backdrop" for="fog_of_war_meters_info">Close</label>
|
||||
</div>
|
||||
|
||||
<input type="checkbox" id="meters_between_routes_info" class="modal-toggle" />
|
||||
<div class="modal focus:z-99" role="dialog">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">Meters between routes</h3>
|
||||
<p class="py-4">
|
||||
Value in meters.
|
||||
</p>
|
||||
<p class="py-4">
|
||||
Points on the map are connected by lines. This value is the maximum distance between two points to be connected by a line. If the distance between two points is greater than this value, they will not be connected, and the line will not be drawn. This allows to split the route into smaller segments, and to avoid drawing lines between two points that are far from each other.
|
||||
</p>
|
||||
</div>
|
||||
<label class="modal-backdrop" for="meters_between_routes_info">Close</label>
|
||||
</div>
|
||||
|
||||
|
||||
<input type="checkbox" id="minutes_between_routes_info" class="modal-toggle" />
|
||||
<div class="modal focus:z-99" role="dialog">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">Minutes between routes</h3>
|
||||
<p class="py-4">
|
||||
Value in minutes.
|
||||
</p>
|
||||
<p class="py-4">
|
||||
Points on the map are connected by lines. This value is the maximum time between two points to be connected by a line. If the time between two points is greater than this value, they will not be connected. This allows to split the route into smaller segments, and to avoid drawing lines between two points that are far in time from each other.
|
||||
</p>
|
||||
</div>
|
||||
<label class="modal-backdrop" for="minutes_between_routes_info">Close</label>
|
||||
</div>
|
||||
|
||||
<input type="checkbox" id="time_threshold_minutes_info" class="modal-toggle" />
|
||||
<div class="modal focus:z-99" role="dialog">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">Visit time threshold</h3>
|
||||
<p class="py-4">
|
||||
Value in minutes.
|
||||
</p>
|
||||
<p class="py-4">
|
||||
This value is the threshold, based on which a visit is calculated. If the time between two consequent points is greater than this value, the visit is considered a new visit. If the time between two points is less than this value, the visit is considered as a continuation of the previous visit.
|
||||
</p>
|
||||
<p class="py-4">
|
||||
For example, if you set this value to 30 minutes, and you have four points with a time difference of 20 minutes between them, they will be considered as one visit. If the time difference between two first points is 20 minutes, and between third and fourth point is 40 minutes, the visit will be split into two visits.
|
||||
</p>
|
||||
<p class="py-4">
|
||||
Default value is 30 minutes.
|
||||
</p>
|
||||
</div>
|
||||
<label class="modal-backdrop" for="time_threshold_minutes_info">Close</label>
|
||||
</div>
|
||||
|
||||
<input type="checkbox" id="merge_threshold_minutes_info" class="modal-toggle" />
|
||||
<div class="modal focus:z-99" role="dialog">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">Merge threshold</h3>
|
||||
<p class="py-4">
|
||||
Value in minutes.
|
||||
</p>
|
||||
<p class="py-4">
|
||||
This value is the threshold, based on which two visits are merged into one. If the time between two consequent visits is less than this value, the visits are merged into one visit. If the time between two visits is greater than this value, the visits are considered as separate visits.
|
||||
</p>
|
||||
<p class="py-4">
|
||||
For example, if you set this value to 30 minutes, and you have two visits with a time difference of 20 minutes between them, they will be merged into one visit. If the time difference between two visits is 40 minutes, the visits will be considered as separate visits.
|
||||
</p>
|
||||
<p class="py-4">
|
||||
Default value is 15 minutes.
|
||||
</p>
|
||||
</div>
|
||||
<label class="modal-backdrop" for="merge_threshold_minutes_info">Close</label>
|
||||
</div>
|
||||
|
|
@ -42,13 +42,10 @@
|
|||
<div
|
||||
class="w-full"
|
||||
data-api_key="<%= current_user.api_key %>"
|
||||
data-route_opacity="<%= current_user.settings['route_opacity'] %>"
|
||||
data-user_settings=<%= current_user.settings.to_json %>
|
||||
data-controller="maps"
|
||||
data-coordinates="<%= @coordinates %>"
|
||||
data-timezone="<%= Rails.configuration.time_zone %>"
|
||||
data-meters_between_routes="<%= current_user.settings['meters_between_routes'] %>"
|
||||
data-minutes_between_routes="<%= current_user.settings['minutes_between_routes'] %>"
|
||||
data-fog_of_war_meters="<%= current_user.settings['fog_of_war_meters'] %>">
|
||||
data-timezone="<%= Rails.configuration.time_zone %>">
|
||||
<div data-maps-target="container" class="h-[25rem] w-auto min-h-screen">
|
||||
<div id="fog" class="fog"></div>
|
||||
</div>
|
||||
|
|
@ -60,4 +57,4 @@
|
|||
<%= render 'shared/right_sidebar' %>
|
||||
</div>
|
||||
|
||||
|
||||
<%= render 'map/settings_modals' %>
|
||||
|
|
|
|||
|
|
@ -7,163 +7,6 @@
|
|||
<div class="card flex-shrink-0 w-full max-w-sm shadow-2xl bg-base-100 px-5 py-5 mx-5">
|
||||
<h2 class="text-2xl font-bold">Edit your Dawarich settings!</h1>
|
||||
<%= form_for :settings, url: settings_path, method: :patch, data: { turbo_method: :patch, turbo: false } do |f| %>
|
||||
<div class="form-control my-2">
|
||||
<%= f.label :meters_between_routes do %>
|
||||
Meters between routes
|
||||
|
||||
<!-- The button to open modal -->
|
||||
<label for="meters_between_routes_info" class="btn">?</label>
|
||||
|
||||
<!-- Put this part before </body> tag -->
|
||||
<input type="checkbox" id="meters_between_routes_info" class="modal-toggle" />
|
||||
<div class="modal" role="dialog">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">Meters between routes</h3>
|
||||
<p class="py-4">
|
||||
Value in meters.
|
||||
</p>
|
||||
<p class="py-4">
|
||||
Points on the map are connected by lines. This value is the maximum distance between two points to be connected by a line. If the distance between two points is greater than this value, they will not be connected, and the line will not be drawn. This allows to split the route into smaller segments, and to avoid drawing lines between two points that are far from each other.
|
||||
</p>
|
||||
</div>
|
||||
<label class="modal-backdrop" for="meters_between_routes_info">Close</label>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= f.number_field :meters_between_routes, value: current_user.settings['meters_between_routes'], class: "input input-bordered" %>
|
||||
</div>
|
||||
|
||||
<div class="form-control my-2">
|
||||
<%= f.label :minutes_between_routes do %>
|
||||
Minutes between routes
|
||||
|
||||
<!-- The button to open modal -->
|
||||
<label for="minutes_between_routes_info" class="btn">?</label>
|
||||
|
||||
<!-- Put this part before </body> tag -->
|
||||
<input type="checkbox" id="minutes_between_routes_info" class="modal-toggle" />
|
||||
<div class="modal" role="dialog">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">Minutes between routes</h3>
|
||||
<p class="py-4">
|
||||
Value in minutes.
|
||||
</p>
|
||||
<p class="py-4">
|
||||
Points on the map are connected by lines. This value is the maximum time between two points to be connected by a line. If the time between two points is greater than this value, they will not be connected. This allows to split the route into smaller segments, and to avoid drawing lines between two points that are far in time from each other.
|
||||
</p>
|
||||
</div>
|
||||
<label class="modal-backdrop" for="minutes_between_routes_info">Close</label>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= f.number_field :minutes_between_routes, value: current_user.settings['minutes_between_routes'], class: "input input-bordered" %>
|
||||
</div>
|
||||
<div class="form-control my-2">
|
||||
<%= f.label :fog_of_war_meters do %>
|
||||
Fog of War meters
|
||||
|
||||
<!-- The button to open modal -->
|
||||
<label for="fog_of_war_meters_info" class="btn">?</label>
|
||||
|
||||
<!-- Put this part before </body> tag -->
|
||||
<input type="checkbox" id="fog_of_war_meters_info" class="modal-toggle" />
|
||||
<div class="modal" role="dialog">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">Fog of War</h3>
|
||||
<p class="py-4">
|
||||
Value in meters.
|
||||
</p>
|
||||
<p class="py-4">
|
||||
Here you can set the radius of the "cleared" area around a point when Fog of War mode is enabled. The area around the point will be cleared, and the rest of the map will be covered with fog. The cleared area will be a circle with the point as the center and the radius as the value you set here.
|
||||
</p>
|
||||
</div>
|
||||
<label class="modal-backdrop" for="fog_of_war_meters_info">Close</label>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= f.number_field :fog_of_war_meters, value: current_user.settings['fog_of_war_meters'], class: "input input-bordered" %>
|
||||
</div>
|
||||
<div class="form-control my-2">
|
||||
<%= f.label :time_threshold_minutes do %>
|
||||
Visit time threshold
|
||||
|
||||
<!-- The button to open modal -->
|
||||
<label for="time_threshold_minutes_info" class="btn">?</label>
|
||||
|
||||
<!-- Put this part before </body> tag -->
|
||||
<input type="checkbox" id="time_threshold_minutes_info" class="modal-toggle" />
|
||||
<div class="modal" role="dialog">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">Visit time threshold</h3>
|
||||
<p class="py-4">
|
||||
Value in minutes.
|
||||
</p>
|
||||
<p class="py-4">
|
||||
This value is the threshold, based on which a visit is calculated. If the time between two consequent points is greater than this value, the visit is considered a new visit. If the time between two points is less than this value, the visit is considered as a continuation of the previous visit.
|
||||
</p>
|
||||
<p class="py-4">
|
||||
For example, if you set this value to 30 minutes, and you have four points with a time difference of 20 minutes between them, they will be considered as one visit. If the time difference between two first points is 20 minutes, and between third and fourth point is 40 minutes, the visit will be split into two visits.
|
||||
</p>
|
||||
<p class="py-4">
|
||||
Default value is 30 minutes.
|
||||
</p>
|
||||
</div>
|
||||
<label class="modal-backdrop" for="time_threshold_minutes_info">Close</label>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= f.number_field :time_threshold_minutes, value: current_user.settings['time_threshold_minutes'], class: "input input-bordered" %>
|
||||
</div>
|
||||
<div class="form-control my-2">
|
||||
<%= f.label :merge_threshold_minutes do %>
|
||||
Merge time threshold
|
||||
|
||||
<!-- The button to open modal -->
|
||||
<label for="merge_threshold_minutes_info" class="btn">?</label>
|
||||
|
||||
<!-- Put this part before </body> tag -->
|
||||
<input type="checkbox" id="merge_threshold_minutes_info" class="modal-toggle" />
|
||||
<div class="modal" role="dialog">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">Merge threshold</h3>
|
||||
<p class="py-4">
|
||||
Value in minutes.
|
||||
</p>
|
||||
<p class="py-4">
|
||||
This value is the threshold, based on which two visits are merged into one. If the time between two consequent visits is less than this value, the visits are merged into one visit. If the time between two visits is greater than this value, the visits are considered as separate visits.
|
||||
</p>
|
||||
<p class="py-4">
|
||||
For example, if you set this value to 30 minutes, and you have two visits with a time difference of 20 minutes between them, they will be merged into one visit. If the time difference between two visits is 40 minutes, the visits will be considered as separate visits.
|
||||
</p>
|
||||
<p class="py-4">
|
||||
Default value is 15 minutes.
|
||||
</p>
|
||||
</div>
|
||||
<label class="modal-backdrop" for="merge_threshold_minutes_info">Close</label>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= f.number_field :merge_threshold_minutes, value: current_user.settings['merge_threshold_minutes'], class: "input input-bordered" %>
|
||||
</div>
|
||||
<div class="form-control my-2">
|
||||
<%= f.label :route_opacity do %>
|
||||
Route opacity percent
|
||||
|
||||
<!-- The button to open modal -->
|
||||
<label for="route_opacity_info" class="btn">?</label>
|
||||
|
||||
<!-- Put this part before </body> tag -->
|
||||
<input type="checkbox" id="route_opacity_info" class="modal-toggle" />
|
||||
<div class="modal" role="dialog">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">Route opacity</h3>
|
||||
<p class="py-4">
|
||||
Value in percent.
|
||||
</p>
|
||||
<p class="py-4">
|
||||
This value is the opacity of the route on the map. The value is in percent, and it can be set from 0 to 100. The default value is 100, which means that the route is fully visible. If you set the value to 0, the route will be invisible.
|
||||
</p>
|
||||
</div>
|
||||
<label class="modal-backdrop" for="route_opacity_info">Close</label>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= f.number_field :route_opacity, value: current_user.settings['route_opacity'], class: "input input-bordered" %>
|
||||
</div>
|
||||
<div class="form-control my-2">
|
||||
<%= f.label :immich_url %>
|
||||
<%= f.text_field :immich_url, value: current_user.settings['immich_url'], class: "input input-bordered", placeholder: 'http://192.168.0.1:2283' %>
|
||||
|
|
|
|||
|
|
@ -56,10 +56,13 @@ Rails.application.routes.draw do
|
|||
|
||||
namespace :api do
|
||||
namespace :v1 do
|
||||
resources :areas, only: %i[index create update destroy]
|
||||
resources :points, only: %i[index destroy]
|
||||
resources :visits, only: %i[update]
|
||||
resources :stats, only: :index
|
||||
patch 'settings', to: 'settings#update'
|
||||
get 'settings', to: 'settings#index'
|
||||
|
||||
resources :areas, only: %i[index create update destroy]
|
||||
resources :points, only: %i[index destroy]
|
||||
resources :visits, only: %i[update]
|
||||
resources :stats, only: :index
|
||||
|
||||
namespace :overland do
|
||||
resources :batches, only: :create
|
||||
|
|
|
|||
|
|
@ -8,6 +8,17 @@ FactoryBot.define do
|
|||
|
||||
password { SecureRandom.hex(8) }
|
||||
|
||||
settings do
|
||||
{
|
||||
route_opacity: '0.5',
|
||||
meters_between_routes: '100',
|
||||
minutes_between_routes: '100',
|
||||
fog_of_war_meters: '100',
|
||||
time_threshold_minutes: '100',
|
||||
merge_threshold_minutes: '100'
|
||||
}
|
||||
end
|
||||
|
||||
trait :admin do
|
||||
admin { true }
|
||||
end
|
||||
|
|
|
|||
48
spec/requests/api/v1/settings_spec.rb
Normal file
48
spec/requests/api/v1/settings_spec.rb
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Api::V1::Settings', type: :request do
|
||||
let!(:user) { create(:user) }
|
||||
let!(:api_key) { user.api_key }
|
||||
|
||||
describe 'PATCH /update' do
|
||||
context 'with valid request' do
|
||||
it 'returns http success' do
|
||||
patch "/api/v1/settings?api_key=#{api_key}", params: { settings: { route_opacity: 0.3 } }
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
it 'updates the settings' do
|
||||
patch "/api/v1/settings?api_key=#{api_key}", params: { settings: { route_opacity: 0.3 } }
|
||||
|
||||
expect(user.reload.settings['route_opacity'].to_f).to eq(0.3)
|
||||
end
|
||||
|
||||
it 'returns the updated settings' do
|
||||
patch "/api/v1/settings?api_key=#{api_key}", params: { settings: { route_opacity: 0.3 } }
|
||||
|
||||
expect(response.parsed_body['settings']['route_opacity'].to_f).to eq(0.3)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid request' do
|
||||
before do
|
||||
allow_any_instance_of(User).to receive(:save).and_return(false)
|
||||
end
|
||||
|
||||
it 'returns http unprocessable entity' do
|
||||
patch "/api/v1/settings?api_key=#{api_key}", params: { settings: { route_opacity: 'invalid' } }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'returns an error message' do
|
||||
patch "/api/v1/settings?api_key=#{api_key}", params: { settings: { route_opacity: 'invalid' } }
|
||||
|
||||
expect(response.parsed_body['message']).to eq('Something went wrong')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -12,6 +12,8 @@ describe 'Points API', type: :request do
|
|||
description: 'Start date (i.e. 2024-02-03T13:00:03Z or 2024-02-03)'
|
||||
parameter name: :end_at, in: :query, type: :string,
|
||||
description: 'End date (i.e. 2024-02-03T13:00:03Z or 2024-02-03)'
|
||||
parameter name: :page, in: :query, type: :integer, required: false, description: 'Page number'
|
||||
parameter name: :per_page, in: :query, type: :integer, required: false, description: 'Number of points per page'
|
||||
response '200', 'points found' do
|
||||
schema type: :array,
|
||||
items: {
|
||||
|
|
|
|||
72
spec/swagger/api/v1/settings_controller_spec.rb
Normal file
72
spec/swagger/api/v1/settings_controller_spec.rb
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'swagger_helper'
|
||||
|
||||
describe 'Settings API', type: :request do
|
||||
path '/api/v1/settings' do
|
||||
patch 'Updates user settings' do
|
||||
request_body_example value: {
|
||||
'settings': {
|
||||
'route_opacity': 0.3,
|
||||
'meters_between_routes': 100,
|
||||
'minutes_between_routes': 100,
|
||||
'fog_of_war_meters': 100,
|
||||
'time_threshold_minutes': 100,
|
||||
'merge_threshold_minutes': 100
|
||||
}
|
||||
}
|
||||
tags 'Settings'
|
||||
consumes 'application/json'
|
||||
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 }
|
||||
},
|
||||
optional: %w[route_opacity meters_between_routes minutes_between_routes fog_of_war_meters
|
||||
time_threshold_minutes merge_threshold_minutes]
|
||||
}
|
||||
parameter name: :api_key, in: :query, type: :string, required: true, description: 'API Key'
|
||||
response '200', 'settings updated' do
|
||||
let(:settings) { { settings: { route_opacity: 0.3 } } }
|
||||
let(:api_key) { create(:user).api_key }
|
||||
|
||||
run_test!
|
||||
end
|
||||
end
|
||||
|
||||
get 'Retrieves user settings' do
|
||||
tags 'Settings'
|
||||
produces 'application/json'
|
||||
parameter name: :api_key, in: :query, type: :string, required: true, description: 'API Key'
|
||||
response '200', 'settings found' do
|
||||
schema type: :object,
|
||||
properties: {
|
||||
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 }
|
||||
},
|
||||
required: %w[route_opacity meters_between_routes minutes_between_routes fog_of_war_meters
|
||||
time_threshold_minutes merge_threshold_minutes]
|
||||
}
|
||||
}
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:settings) { { settings: user.settings } }
|
||||
let(:api_key) { user.api_key }
|
||||
|
||||
run_test!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -326,6 +326,18 @@ paths:
|
|||
description: End date (i.e. 2024-02-03T13:00:03Z or 2024-02-03)
|
||||
schema:
|
||||
type: string
|
||||
- name: page
|
||||
in: query
|
||||
required: false
|
||||
description: Page number
|
||||
schema:
|
||||
type: integer
|
||||
- name: per_page
|
||||
in: query
|
||||
required: false
|
||||
description: Number of points per page
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: points found
|
||||
|
|
@ -415,6 +427,98 @@ paths:
|
|||
responses:
|
||||
'200':
|
||||
description: point deleted
|
||||
"/api/v1/settings":
|
||||
patch:
|
||||
summary: Updates user settings
|
||||
tags:
|
||||
- Settings
|
||||
parameters:
|
||||
- name: api_key
|
||||
in: query
|
||||
required: true
|
||||
description: API Key
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: settings updated
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
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
|
||||
optional:
|
||||
- route_opacity
|
||||
- meters_between_routes
|
||||
- minutes_between_routes
|
||||
- fog_of_war_meters
|
||||
- time_threshold_minutes
|
||||
- merge_threshold_minutes
|
||||
examples:
|
||||
'0':
|
||||
summary: Updates user settings
|
||||
value:
|
||||
settings:
|
||||
route_opacity: 0.3
|
||||
meters_between_routes: 100
|
||||
minutes_between_routes: 100
|
||||
fog_of_war_meters: 100
|
||||
time_threshold_minutes: 100
|
||||
merge_threshold_minutes: 100
|
||||
get:
|
||||
summary: Retrieves user settings
|
||||
tags:
|
||||
- Settings
|
||||
parameters:
|
||||
- name: api_key
|
||||
in: query
|
||||
required: true
|
||||
description: API Key
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: settings found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
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
|
||||
required:
|
||||
- route_opacity
|
||||
- meters_between_routes
|
||||
- minutes_between_routes
|
||||
- fog_of_war_meters
|
||||
- time_threshold_minutes
|
||||
- merge_threshold_minutes
|
||||
"/api/v1/stats":
|
||||
get:
|
||||
summary: Retrieves all stats
|
||||
|
|
|
|||
Loading…
Reference in a new issue