mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Merge pull request #1020 from MeijiRestored/dev
Add speed color scale setting
This commit is contained in:
commit
2d9882810c
5 changed files with 225 additions and 16 deletions
File diff suppressed because one or more lines are too long
|
|
@ -30,7 +30,7 @@ class Api::V1::SettingsController < ApiController
|
||||||
:time_threshold_minutes, :merge_threshold_minutes, :route_opacity,
|
:time_threshold_minutes, :merge_threshold_minutes, :route_opacity,
|
||||||
:preferred_map_layer, :points_rendering_mode, :live_map_enabled,
|
:preferred_map_layer, :points_rendering_mode, :live_map_enabled,
|
||||||
:immich_url, :immich_api_key, :photoprism_url, :photoprism_api_key,
|
:immich_url, :immich_api_key, :photoprism_url, :photoprism_api_key,
|
||||||
:speed_colored_routes
|
:speed_colored_routes, :speed_color_scale
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,10 @@ import { createMarkersArray } from "../maps/markers";
|
||||||
import {
|
import {
|
||||||
createPolylinesLayer,
|
createPolylinesLayer,
|
||||||
updatePolylinesOpacity,
|
updatePolylinesOpacity,
|
||||||
updatePolylinesColors
|
updatePolylinesColors,
|
||||||
|
colorFormatEncode,
|
||||||
|
colorFormatDecode,
|
||||||
|
colorStopsFallback
|
||||||
} from "../maps/polylines";
|
} from "../maps/polylines";
|
||||||
|
|
||||||
import { fetchAndDrawAreas, handleAreaCreated } from "../maps/areas";
|
import { fetchAndDrawAreas, handleAreaCreated } from "../maps/areas";
|
||||||
|
|
@ -47,6 +50,7 @@ export default class extends BaseController {
|
||||||
this.liveMapEnabled = this.userSettings.live_map_enabled || false;
|
this.liveMapEnabled = this.userSettings.live_map_enabled || false;
|
||||||
this.countryCodesMap = countryCodesMap();
|
this.countryCodesMap = countryCodesMap();
|
||||||
this.speedColoredPolylines = this.userSettings.speed_colored_routes || false;
|
this.speedColoredPolylines = this.userSettings.speed_colored_routes || false;
|
||||||
|
this.speedColorScale = this.userSettings.speed_color_scale || colorFormatEncode(speedColorScaleDefault);
|
||||||
|
|
||||||
this.center = this.markers[this.markers.length - 1] || [52.514568, 13.350111];
|
this.center = this.markers[this.markers.length - 1] || [52.514568, 13.350111];
|
||||||
|
|
||||||
|
|
@ -699,7 +703,7 @@ export default class extends BaseController {
|
||||||
|
|
||||||
// Form HTML
|
// Form HTML
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<form id="settings-form" class="w-48">
|
<form id="settings-form" class="w-48 h-144 overflow-y-auto">
|
||||||
<label for="route-opacity">Route Opacity</label>
|
<label for="route-opacity">Route Opacity</label>
|
||||||
<div class="join">
|
<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}">
|
<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}">
|
||||||
|
|
@ -768,7 +772,16 @@ export default class extends BaseController {
|
||||||
<input type="checkbox" id="speed_colored_routes" name="speed_colored_routes" class='w-4' style="width: 20px;" ${this.speedColoredRoutesChecked()} />
|
<input type="checkbox" id="speed_colored_routes" name="speed_colored_routes" class='w-4' style="width: 20px;" ${this.speedColoredRoutesChecked()} />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<button type="submit">Update</button>
|
<label for="speed_color_scale">Speed color scale</label>
|
||||||
|
<div class="join">
|
||||||
|
<input type="text" class="join-item input input-ghost focus:input-ghost input-xs input-bordered w-full max-w-xs" id="speed_color_scale" name="speed_color_scale" min="5" max="100" step="1" value="${this.speedColorScale}">
|
||||||
|
<label for="speed_color_scale_info" class="btn-xs join-item">?</label>
|
||||||
|
</div>
|
||||||
|
<button type="button" id="edit-gradient-btn" class="btn btn-xs mt-2">Edit Scale</button>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-xs mt-2">Update</button>
|
||||||
</form>
|
</form>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -781,6 +794,12 @@ export default class extends BaseController {
|
||||||
// Prevent map interactions when interacting with the form
|
// Prevent map interactions when interacting with the form
|
||||||
L.DomEvent.disableClickPropagation(div);
|
L.DomEvent.disableClickPropagation(div);
|
||||||
|
|
||||||
|
// Attach event listener to the "Edit Gradient" button:
|
||||||
|
const editBtn = div.querySelector("#edit-gradient-btn");
|
||||||
|
if (editBtn) {
|
||||||
|
editBtn.addEventListener("click", this.showGradientEditor.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
// Add event listener to the form submission
|
// Add event listener to the form submission
|
||||||
div.querySelector('#settings-form').addEventListener(
|
div.querySelector('#settings-form').addEventListener(
|
||||||
'submit', this.updateSettings.bind(this)
|
'submit', this.updateSettings.bind(this)
|
||||||
|
|
@ -829,7 +848,8 @@ export default class extends BaseController {
|
||||||
merge_threshold_minutes: event.target.merge_threshold_minutes.value,
|
merge_threshold_minutes: event.target.merge_threshold_minutes.value,
|
||||||
points_rendering_mode: event.target.points_rendering_mode.value,
|
points_rendering_mode: event.target.points_rendering_mode.value,
|
||||||
live_map_enabled: event.target.live_map_enabled.checked,
|
live_map_enabled: event.target.live_map_enabled.checked,
|
||||||
speed_colored_routes: event.target.speed_colored_routes.checked
|
speed_colored_routes: event.target.speed_colored_routes.checked,
|
||||||
|
speed_color_scale: event.target.speed_color_scale.value
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
@ -866,7 +886,18 @@ export default class extends BaseController {
|
||||||
if (this.polylinesLayer) {
|
if (this.polylinesLayer) {
|
||||||
updatePolylinesColors(
|
updatePolylinesColors(
|
||||||
this.polylinesLayer,
|
this.polylinesLayer,
|
||||||
newSettings.speed_colored_routes
|
newSettings.speed_colored_routes,
|
||||||
|
newSettings.speed_color_scale
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newSettings.speed_color_scale !== this.userSettings.speed_color_scale) {
|
||||||
|
if (this.polylinesLayer) {
|
||||||
|
updatePolylinesColors(
|
||||||
|
this.polylinesLayer,
|
||||||
|
newSettings.speed_colored_routes,
|
||||||
|
newSettings.speed_color_scale
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1352,4 +1383,143 @@ export default class extends BaseController {
|
||||||
|
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showGradientEditor() {
|
||||||
|
const modal = document.createElement("div");
|
||||||
|
modal.id = "gradient-editor-modal";
|
||||||
|
Object.assign(modal.style, {
|
||||||
|
position: "fixed",
|
||||||
|
top: "0",
|
||||||
|
left: "0",
|
||||||
|
right: "0",
|
||||||
|
bottom: "0",
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
zIndex: "100",
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = document.createElement("div");
|
||||||
|
Object.assign(content.style, {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
padding: "20px",
|
||||||
|
borderRadius: "5px",
|
||||||
|
minWidth: "300px",
|
||||||
|
maxHeight: "80vh",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
});
|
||||||
|
|
||||||
|
const title = document.createElement("h2");
|
||||||
|
title.textContent = "Edit Speed Color Scale";
|
||||||
|
content.appendChild(title);
|
||||||
|
|
||||||
|
const gradientContainer = document.createElement("div");
|
||||||
|
gradientContainer.id = "gradient-editor-container";
|
||||||
|
Object.assign(gradientContainer.style, {
|
||||||
|
marginTop: "15px",
|
||||||
|
overflowY: "auto",
|
||||||
|
flex: "1",
|
||||||
|
border: "1px solid #ccc",
|
||||||
|
padding: "5px",
|
||||||
|
});
|
||||||
|
|
||||||
|
const createRow = (stop = { speed: 0, color: "#000000" }) => {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.style.display = "flex";
|
||||||
|
row.style.alignItems = "center";
|
||||||
|
row.style.gap = "10px";
|
||||||
|
row.style.marginBottom = "8px";
|
||||||
|
|
||||||
|
const speedInput = document.createElement("input");
|
||||||
|
speedInput.type = "number";
|
||||||
|
speedInput.value = stop.speed;
|
||||||
|
speedInput.style.width = "70px";
|
||||||
|
|
||||||
|
const colorInput = document.createElement("input");
|
||||||
|
colorInput.type = "color";
|
||||||
|
colorInput.value = stop.color;
|
||||||
|
colorInput.style.width = "70px";
|
||||||
|
|
||||||
|
const removeBtn = document.createElement("button");
|
||||||
|
removeBtn.textContent = "x";
|
||||||
|
removeBtn.style.color = "#cc3311";
|
||||||
|
removeBtn.style.flexShrink = "0";
|
||||||
|
removeBtn.addEventListener("click", () => {
|
||||||
|
if (gradientContainer.childElementCount > 1) {
|
||||||
|
gradientContainer.removeChild(row);
|
||||||
|
} else {
|
||||||
|
showFlashMessage('error', 'At least one gradient stop is required.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
row.appendChild(speedInput);
|
||||||
|
row.appendChild(colorInput);
|
||||||
|
row.appendChild(removeBtn);
|
||||||
|
return row;
|
||||||
|
};
|
||||||
|
|
||||||
|
let stops;
|
||||||
|
try {
|
||||||
|
stops = colorFormatDecode(this.speedColorScale);
|
||||||
|
} catch (error) {
|
||||||
|
stops = colorStopsFallback;
|
||||||
|
}
|
||||||
|
stops.forEach(stop => {
|
||||||
|
const row = createRow(stop);
|
||||||
|
gradientContainer.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
content.appendChild(gradientContainer);
|
||||||
|
|
||||||
|
const addRowBtn = document.createElement("button");
|
||||||
|
addRowBtn.textContent = "Add Row";
|
||||||
|
addRowBtn.style.marginTop = "10px";
|
||||||
|
addRowBtn.addEventListener("click", () => {
|
||||||
|
const newRow = createRow({ speed: 0, color: "#000000" });
|
||||||
|
gradientContainer.appendChild(newRow);
|
||||||
|
});
|
||||||
|
content.appendChild(addRowBtn);
|
||||||
|
|
||||||
|
const btnContainer = document.createElement("div");
|
||||||
|
btnContainer.style.display = "flex";
|
||||||
|
btnContainer.style.justifyContent = "flex-end";
|
||||||
|
btnContainer.style.gap = "10px";
|
||||||
|
btnContainer.style.marginTop = "15px";
|
||||||
|
|
||||||
|
const cancelBtn = document.createElement("button");
|
||||||
|
cancelBtn.textContent = "Cancel";
|
||||||
|
cancelBtn.addEventListener("click", () => {
|
||||||
|
document.body.removeChild(modal);
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveBtn = document.createElement("button");
|
||||||
|
saveBtn.textContent = "Save";
|
||||||
|
saveBtn.addEventListener("click", () => {
|
||||||
|
const newStops = [];
|
||||||
|
gradientContainer.querySelectorAll("div").forEach(row => {
|
||||||
|
const inputs = row.querySelectorAll("input");
|
||||||
|
const speed = Number(inputs[0].value);
|
||||||
|
const color = inputs[1].value;
|
||||||
|
newStops.push({ speed, color });
|
||||||
|
});
|
||||||
|
|
||||||
|
const newGradient = colorFormatEncode(newStops);
|
||||||
|
|
||||||
|
this.speedColorScale = newGradient;
|
||||||
|
const speedColorScaleInput = document.getElementById("speed_color_scale");
|
||||||
|
if (speedColorScaleInput) {
|
||||||
|
speedColorScaleInput.value = newGradient;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.removeChild(modal);
|
||||||
|
});
|
||||||
|
|
||||||
|
btnContainer.appendChild(cancelBtn);
|
||||||
|
btnContainer.appendChild(saveBtn);
|
||||||
|
content.appendChild(btnContainer);
|
||||||
|
modal.appendChild(content);
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,22 +25,44 @@ export function calculateSpeed(point1, point2) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optimize getSpeedColor by pre-calculating color stops
|
// Optimize getSpeedColor by pre-calculating color stops
|
||||||
const colorStops = [
|
export const colorStopsFallback = [
|
||||||
{ speed: 0, color: '#00ff00' }, // Stationary/very slow (green)
|
{ speed: 0, color: '#00ff00' }, // Stationary/very slow (green)
|
||||||
{ speed: 15, color: '#00ffff' }, // Walking/jogging (cyan)
|
{ speed: 15, color: '#00ffff' }, // Walking/jogging (cyan)
|
||||||
{ speed: 30, color: '#ff00ff' }, // Cycling/slow driving (magenta)
|
{ speed: 30, color: '#ff00ff' }, // Cycling/slow driving (magenta)
|
||||||
{ speed: 50, color: '#ffff00' }, // Urban driving (yellow)
|
{ speed: 50, color: '#ffff00' }, // Urban driving (yellow)
|
||||||
{ speed: 100, color: '#ff3300' } // Highway driving (red)
|
{ speed: 100, color: '#ff3300' } // Highway driving (red)
|
||||||
].map(stop => ({
|
];
|
||||||
...stop,
|
|
||||||
rgb: hexToRGB(stop.color)
|
|
||||||
}));
|
|
||||||
|
|
||||||
export function getSpeedColor(speedKmh, useSpeedColors) {
|
export function colorFormatEncode(arr) {
|
||||||
|
return arr.map(item => `${item.speed}:${item.color}`).join('|');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function colorFormatDecode(str) {
|
||||||
|
return str.split('|').map(segment => {
|
||||||
|
const [speed, color] = segment.split(':');
|
||||||
|
return { speed: Number(speed), color };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSpeedColor(speedKmh, useSpeedColors, speedColorScale) {
|
||||||
if (!useSpeedColors) {
|
if (!useSpeedColors) {
|
||||||
return '#0000ff';
|
return '#0000ff';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let colorStops;
|
||||||
|
|
||||||
|
try {
|
||||||
|
colorStops = colorFormatDecode(speedColorScale).map(stop => ({
|
||||||
|
...stop,
|
||||||
|
rgb: hexToRGB(stop.color)
|
||||||
|
}));;
|
||||||
|
} catch (error) { // If user has given invalid values
|
||||||
|
colorStops = colorStopsFallback.map(stop => ({
|
||||||
|
...stop,
|
||||||
|
rgb: hexToRGB(stop.color)
|
||||||
|
}));;
|
||||||
|
}
|
||||||
|
|
||||||
// Find the appropriate color segment
|
// Find the appropriate color segment
|
||||||
for (let i = 1; i < colorStops.length; i++) {
|
for (let i = 1; i < colorStops.length; i++) {
|
||||||
if (speedKmh <= colorStops[i].speed) {
|
if (speedKmh <= colorStops[i].speed) {
|
||||||
|
|
@ -388,7 +410,7 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS
|
||||||
|
|
||||||
for (let i = 0; i < polylineCoordinates.length - 1; i++) {
|
for (let i = 0; i < polylineCoordinates.length - 1; i++) {
|
||||||
const speed = calculateSpeed(polylineCoordinates[i], polylineCoordinates[i + 1]);
|
const speed = calculateSpeed(polylineCoordinates[i], polylineCoordinates[i + 1]);
|
||||||
const color = getSpeedColor(speed, userSettings.speed_colored_routes);
|
const color = getSpeedColor(speed, userSettings.speed_colored_routes, userSettings.speed_color_scale);
|
||||||
|
|
||||||
const segment = L.polyline(
|
const segment = L.polyline(
|
||||||
[
|
[
|
||||||
|
|
@ -466,7 +488,7 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS
|
||||||
return layerGroup;
|
return layerGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updatePolylinesColors(polylinesLayer, useSpeedColors) {
|
export function updatePolylinesColors(polylinesLayer, useSpeedColors, speedColorScale) {
|
||||||
const defaultStyle = {
|
const defaultStyle = {
|
||||||
color: '#0000ff',
|
color: '#0000ff',
|
||||||
originalColor: '#0000ff'
|
originalColor: '#0000ff'
|
||||||
|
|
@ -496,7 +518,7 @@ export function updatePolylinesColors(polylinesLayer, useSpeedColors) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const speed = segment.options.speed || 0;
|
const speed = segment.options.speed || 0;
|
||||||
const newColor = getSpeedColor(speed, true);
|
const newColor = getSpeedColor(speed, true, speedColorScale);
|
||||||
|
|
||||||
// Reuse style object
|
// Reuse style object
|
||||||
styleObj.color = newColor;
|
styleObj.color = newColor;
|
||||||
|
|
|
||||||
|
|
@ -141,3 +141,20 @@
|
||||||
</div>
|
</div>
|
||||||
<label class="modal-backdrop" for="speed_colored_routes_info">Close</label>
|
<label class="modal-backdrop" for="speed_colored_routes_info">Close</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input type="checkbox" id="speed_color_scale_info" class="modal-toggle" />
|
||||||
|
<div class="modal focus:z-99" role="dialog">
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3 class="text-lg font-bold">Speed color scale</h3>
|
||||||
|
<p class="py-4">
|
||||||
|
Value in format <code>speed_kmh:hex_color|...</code>.
|
||||||
|
</p>
|
||||||
|
<p class="py-4">
|
||||||
|
Here you can set a custom color scale for speed colored routes. It uses color stops at specified km/h values and creates a gradient from it. The default value is <code>0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300</code>
|
||||||
|
</p>
|
||||||
|
<p class="py-4">
|
||||||
|
You can also use the 'Edit Scale' button to edit it using an UI.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<label class="modal-backdrop" for="speed_color_scale_info">Close</label>
|
||||||
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue