diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index 57f37b67..7b7ecc4e 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -9,7 +9,9 @@ import { createPolylinesLayer, updatePolylinesOpacity, updatePolylinesColors, - colorFormatEncode + colorFormatEncode, + colorFormatDecode, + colorStopsFallback } from "../maps/polylines"; import { fetchAndDrawAreas, handleAreaCreated } from "../maps/areas"; @@ -37,14 +39,6 @@ export default class extends BaseController { super.connect(); console.log("Map controller connected"); - const speedColorScaleDefault = [ - { speed: 0, color: '#00ff00' }, - { speed: 15, color: '#00ffff' }, - { speed: 30, color: '#ff00ff' }, - { speed: 50, color: '#ffff00' }, - { speed: 100, color: '#ff3300' } - ]; - this.apiKey = this.element.dataset.api_key; this.markers = JSON.parse(this.element.dataset.coordinates); this.timezone = this.element.dataset.timezone; @@ -783,9 +777,11 @@ export default class extends BaseController { + +
- + `; @@ -798,6 +794,12 @@ export default class extends BaseController { // Prevent map interactions when interacting with the form 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 div.querySelector('#settings-form').addEventListener( 'submit', this.updateSettings.bind(this) @@ -1381,4 +1383,143 @@ export default class extends BaseController { 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); + } } diff --git a/app/javascript/maps/polylines.js b/app/javascript/maps/polylines.js index 4a3cf888..197d7655 100644 --- a/app/javascript/maps/polylines.js +++ b/app/javascript/maps/polylines.js @@ -25,16 +25,13 @@ export function calculateSpeed(point1, point2) { } // Optimize getSpeedColor by pre-calculating color stops -const colorStopsFallback = [ +export const colorStopsFallback = [ { speed: 0, color: '#00ff00' }, // Stationary/very slow (green) { speed: 15, color: '#00ffff' }, // Walking/jogging (cyan) { speed: 30, color: '#ff00ff' }, // Cycling/slow driving (magenta) { speed: 50, color: '#ffff00' }, // Urban driving (yellow) { speed: 100, color: '#ff3300' } // Highway driving (red) -].map(stop => ({ - ...stop, - rgb: hexToRGB(stop.color) -})); +]; export function colorFormatEncode(arr) { return arr.map(item => `${item.speed}:${item.color}`).join('|'); @@ -60,7 +57,10 @@ export function getSpeedColor(speedKmh, useSpeedColors, speedColorScale) { rgb: hexToRGB(stop.color) }));; } catch (error) { // If user has given invalid values - colorStops = colorStopsFallback; + colorStops = colorStopsFallback.map(stop => ({ + ...stop, + rgb: hexToRGB(stop.color) + }));; } // Find the appropriate color segment diff --git a/app/views/map/_settings_modals.html.erb b/app/views/map/_settings_modals.html.erb index 8436795d..6703f5f2 100644 --- a/app/views/map/_settings_modals.html.erb +++ b/app/views/map/_settings_modals.html.erb @@ -152,6 +152,9 @@

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 0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300

+

+ You can also use the 'Edit Scale' button to edit it using an UI. +