mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Update speed routes
This commit is contained in:
parent
529eee775a
commit
541488e6ce
6 changed files with 590 additions and 5 deletions
File diff suppressed because one or more lines are too long
|
|
@ -50,7 +50,12 @@ export default class extends Controller {
|
|||
'areasToggle',
|
||||
// 'tracksToggle',
|
||||
'fogToggle',
|
||||
'scratchToggle'
|
||||
'scratchToggle',
|
||||
// Speed-colored routes
|
||||
'routesOptions',
|
||||
'speedColoredToggle',
|
||||
'speedColorScaleContainer',
|
||||
'speedColorScaleInput'
|
||||
]
|
||||
|
||||
async connect() {
|
||||
|
|
@ -118,7 +123,8 @@ export default class extends Controller {
|
|||
areasToggle: 'areasEnabled',
|
||||
// tracksToggle: 'tracksEnabled',
|
||||
fogToggle: 'fogEnabled',
|
||||
scratchToggle: 'scratchEnabled'
|
||||
scratchToggle: 'scratchEnabled',
|
||||
speedColoredToggle: 'speedColoredRoutesEnabled'
|
||||
}
|
||||
|
||||
Object.entries(toggleMap).forEach(([targetName, settingKey]) => {
|
||||
|
|
@ -173,6 +179,16 @@ export default class extends Controller {
|
|||
}
|
||||
}
|
||||
|
||||
// Sync speed-colored routes settings
|
||||
if (this.hasSpeedColorScaleInputTarget) {
|
||||
const colorScale = this.settings.speedColorScale || '0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300'
|
||||
this.speedColorScaleInputTarget.value = colorScale
|
||||
}
|
||||
if (this.hasSpeedColorScaleContainerTarget && this.hasSpeedColoredToggleTarget) {
|
||||
const isEnabled = this.speedColoredToggleTarget.checked
|
||||
this.speedColorScaleContainerTarget.classList.toggle('hidden', !isEnabled)
|
||||
}
|
||||
|
||||
// Sync points rendering mode radio buttons
|
||||
const pointsRenderingRadios = this.element.querySelectorAll('input[name="pointsRenderingMode"]')
|
||||
pointsRenderingRadios.forEach(radio => {
|
||||
|
|
@ -426,6 +442,11 @@ export default class extends Controller {
|
|||
routesLayer.toggle(visible)
|
||||
}
|
||||
|
||||
// Show/hide routes options panel
|
||||
if (this.hasRoutesOptionsTarget) {
|
||||
this.routesOptionsTarget.style.display = visible ? 'block' : 'none'
|
||||
}
|
||||
|
||||
// Save setting
|
||||
SettingsManager.updateSetting('routesVisible', visible)
|
||||
}
|
||||
|
|
@ -747,4 +768,211 @@ export default class extends Controller {
|
|||
Toast.error('Failed to load scratch layer')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle speed-colored routes
|
||||
*/
|
||||
async toggleSpeedColoredRoutes(event) {
|
||||
const enabled = event.target.checked
|
||||
SettingsManager.updateSetting('speedColoredRoutesEnabled', enabled)
|
||||
|
||||
// Show/hide color scale container
|
||||
if (this.hasSpeedColorScaleContainerTarget) {
|
||||
this.speedColorScaleContainerTarget.classList.toggle('hidden', !enabled)
|
||||
}
|
||||
|
||||
// Reload routes with speed colors
|
||||
await this.reloadRoutes()
|
||||
}
|
||||
|
||||
/**
|
||||
* Open speed color editor modal
|
||||
*/
|
||||
openSpeedColorEditor() {
|
||||
const currentScale = this.speedColorScaleInputTarget.value || '0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300'
|
||||
|
||||
// Create modal if it doesn't exist
|
||||
let modal = document.getElementById('speed-color-editor-modal')
|
||||
if (!modal) {
|
||||
modal = this.createSpeedColorEditorModal(currentScale)
|
||||
document.body.appendChild(modal)
|
||||
} else {
|
||||
// Update existing modal with current scale
|
||||
const controller = this.application.getControllerForElementAndIdentifier(modal, 'speed-color-editor')
|
||||
if (controller) {
|
||||
controller.colorStopsValue = currentScale
|
||||
controller.loadColorStops()
|
||||
}
|
||||
}
|
||||
|
||||
// Show modal
|
||||
const checkbox = modal.querySelector('.modal-toggle')
|
||||
if (checkbox) {
|
||||
checkbox.checked = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create speed color editor modal element
|
||||
*/
|
||||
createSpeedColorEditorModal(currentScale) {
|
||||
const modal = document.createElement('div')
|
||||
modal.id = 'speed-color-editor-modal'
|
||||
modal.setAttribute('data-controller', 'speed-color-editor')
|
||||
modal.setAttribute('data-speed-color-editor-color-stops-value', currentScale)
|
||||
modal.setAttribute('data-action', 'speed-color-editor:save->maps-v2#handleSpeedColorSave')
|
||||
|
||||
modal.innerHTML = `
|
||||
<input type="checkbox" id="speed-color-editor-toggle" class="modal-toggle" />
|
||||
<div class="modal" role="dialog" data-speed-color-editor-target="modal">
|
||||
<div class="modal-box max-w-2xl">
|
||||
<h3 class="text-lg font-bold mb-4">Edit Speed Color Gradient</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Gradient Preview -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Preview</span>
|
||||
</label>
|
||||
<div class="h-12 rounded-lg border-2 border-base-300"
|
||||
data-speed-color-editor-target="preview"></div>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">This gradient will be applied to routes based on speed</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Color Stops List -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Color Stops</span>
|
||||
</label>
|
||||
<div class="space-y-2" data-speed-color-editor-target="stopsList"></div>
|
||||
</div>
|
||||
|
||||
<!-- Add Stop Button -->
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline w-full"
|
||||
data-action="click->speed-color-editor#addStop">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Add Color Stop
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
<button type="button"
|
||||
class="btn btn-ghost"
|
||||
data-action="click->speed-color-editor#resetToDefault">
|
||||
Reset to Default
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn"
|
||||
data-action="click->speed-color-editor#close">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-primary"
|
||||
data-action="click->speed-color-editor#save">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<label class="modal-backdrop" for="speed-color-editor-toggle"></label>
|
||||
</div>
|
||||
`
|
||||
|
||||
return modal
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle speed color save event from editor
|
||||
*/
|
||||
handleSpeedColorSave(event) {
|
||||
const newScale = event.detail.colorStops
|
||||
|
||||
// Save to settings
|
||||
this.speedColorScaleInputTarget.value = newScale
|
||||
SettingsManager.updateSetting('speedColorScale', newScale)
|
||||
|
||||
// Reload routes if speed colors are enabled
|
||||
if (this.speedColoredToggleTarget.checked) {
|
||||
this.reloadRoutes()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload routes layer
|
||||
*/
|
||||
async reloadRoutes() {
|
||||
this.showLoading('Reloading routes...')
|
||||
|
||||
try {
|
||||
const pointsLayer = this.layerManager.getLayer('points')
|
||||
const points = pointsLayer?.data?.features?.map(f => ({
|
||||
latitude: f.geometry.coordinates[1],
|
||||
longitude: f.geometry.coordinates[0],
|
||||
timestamp: f.properties.timestamp
|
||||
})) || []
|
||||
|
||||
// Get route generation settings
|
||||
const distanceThresholdMeters = this.settings.metersBetweenRoutes || 500
|
||||
const timeThresholdMinutes = this.settings.minutesBetweenRoutes || 60
|
||||
|
||||
// Import speed colors utility
|
||||
const { calculateSpeed, getSpeedColor } = await import('maps_v2/utils/speed_colors')
|
||||
|
||||
// Generate routes with speed coloring if enabled
|
||||
const routesGeoJSON = await this.generateRoutesWithSpeedColors(
|
||||
points,
|
||||
{ distanceThresholdMeters, timeThresholdMinutes },
|
||||
calculateSpeed,
|
||||
getSpeedColor
|
||||
)
|
||||
|
||||
// Update routes layer
|
||||
this.layerManager.updateLayer('routes', routesGeoJSON)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to reload routes:', error)
|
||||
Toast.error('Failed to reload routes')
|
||||
} finally {
|
||||
this.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate routes with speed coloring
|
||||
*/
|
||||
async generateRoutesWithSpeedColors(points, options, calculateSpeed, getSpeedColor) {
|
||||
const { RoutesLayer } = await import('maps_v2/layers/routes_layer')
|
||||
const useSpeedColors = this.settings.speedColoredRoutesEnabled || false
|
||||
const speedColorScale = this.settings.speedColorScale || '0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300'
|
||||
|
||||
// Use RoutesLayer static method to generate basic routes
|
||||
const routesGeoJSON = RoutesLayer.pointsToRoutes(points, options)
|
||||
|
||||
if (!useSpeedColors) {
|
||||
return routesGeoJSON
|
||||
}
|
||||
|
||||
// Add speed colors to route segments
|
||||
routesGeoJSON.features = routesGeoJSON.features.map((feature, index) => {
|
||||
const segment = points.slice(
|
||||
points.findIndex(p => p.timestamp === feature.properties.startTime),
|
||||
points.findIndex(p => p.timestamp === feature.properties.endTime) + 1
|
||||
)
|
||||
|
||||
if (segment.length >= 2) {
|
||||
const speed = calculateSpeed(segment[0], segment[segment.length - 1])
|
||||
const color = getSpeedColor(speed, useSpeedColors, speedColorScale)
|
||||
feature.properties.speed = speed
|
||||
feature.properties.color = color
|
||||
}
|
||||
|
||||
return feature
|
||||
})
|
||||
|
||||
return routesGeoJSON
|
||||
}
|
||||
}
|
||||
|
|
|
|||
184
app/javascript/controllers/speed_color_editor_controller.js
Normal file
184
app/javascript/controllers/speed_color_editor_controller.js
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
import { Controller } from '@hotwired/stimulus'
|
||||
|
||||
/**
|
||||
* Speed Color Editor Controller
|
||||
* Manages the gradient editor modal for speed-colored routes
|
||||
*/
|
||||
export default class extends Controller {
|
||||
static targets = ['modal', 'stopsList', 'preview']
|
||||
static values = {
|
||||
colorStops: String
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.loadColorStops()
|
||||
}
|
||||
|
||||
loadColorStops() {
|
||||
const stopsString = this.colorStopsValue || '0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300'
|
||||
this.stops = this.parseColorStops(stopsString)
|
||||
this.renderStops()
|
||||
this.updatePreview()
|
||||
}
|
||||
|
||||
parseColorStops(stopsString) {
|
||||
return stopsString.split('|').map(segment => {
|
||||
const [speed, color] = segment.split(':')
|
||||
return { speed: Number(speed), color }
|
||||
})
|
||||
}
|
||||
|
||||
serializeColorStops() {
|
||||
return this.stops.map(stop => `${stop.speed}:${stop.color}`).join('|')
|
||||
}
|
||||
|
||||
renderStops() {
|
||||
if (!this.hasStopsListTarget) return
|
||||
|
||||
this.stopsListTarget.innerHTML = this.stops.map((stop, index) => `
|
||||
<div class="flex items-center gap-3 p-3 bg-base-200 rounded-lg" data-index="${index}">
|
||||
<div class="flex-1">
|
||||
<label class="label">
|
||||
<span class="label-text text-sm">Speed (km/h)</span>
|
||||
</label>
|
||||
<input type="number"
|
||||
class="input input-bordered input-sm w-full"
|
||||
value="${stop.speed}"
|
||||
min="0"
|
||||
max="200"
|
||||
data-action="input->speed-color-editor#updateSpeed"
|
||||
data-index="${index}" />
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<label class="label">
|
||||
<span class="label-text text-sm">Color</span>
|
||||
</label>
|
||||
<div class="flex gap-2 items-center">
|
||||
<input type="color"
|
||||
class="w-12 h-10 rounded cursor-pointer border-2 border-base-300"
|
||||
value="${stop.color}"
|
||||
data-action="input->speed-color-editor#updateColor"
|
||||
data-index="${index}" />
|
||||
<input type="text"
|
||||
class="input input-bordered input-sm w-24 font-mono text-xs"
|
||||
value="${stop.color}"
|
||||
pattern="^#[0-9A-Fa-f]{6}$"
|
||||
data-action="input->speed-color-editor#updateColorText"
|
||||
data-index="${index}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-ghost btn-circle text-error mt-6"
|
||||
data-action="click->speed-color-editor#removeStop"
|
||||
data-index="${index}"
|
||||
${this.stops.length <= 2 ? 'disabled' : ''}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`).join('')
|
||||
}
|
||||
|
||||
updateSpeed(event) {
|
||||
const index = parseInt(event.target.dataset.index)
|
||||
this.stops[index].speed = Number(event.target.value)
|
||||
this.updatePreview()
|
||||
}
|
||||
|
||||
updateColor(event) {
|
||||
const index = parseInt(event.target.dataset.index)
|
||||
const color = event.target.value
|
||||
this.stops[index].color = color
|
||||
|
||||
// Update text input
|
||||
const textInput = event.target.parentElement.querySelector('input[type="text"]')
|
||||
if (textInput) {
|
||||
textInput.value = color
|
||||
}
|
||||
|
||||
this.updatePreview()
|
||||
}
|
||||
|
||||
updateColorText(event) {
|
||||
const index = parseInt(event.target.dataset.index)
|
||||
const color = event.target.value
|
||||
|
||||
if (/^#[0-9A-Fa-f]{6}$/.test(color)) {
|
||||
this.stops[index].color = color
|
||||
|
||||
// Update color picker
|
||||
const colorInput = event.target.parentElement.querySelector('input[type="color"]')
|
||||
if (colorInput) {
|
||||
colorInput.value = color
|
||||
}
|
||||
|
||||
this.updatePreview()
|
||||
}
|
||||
}
|
||||
|
||||
addStop() {
|
||||
// Find a good speed value between existing stops
|
||||
const lastStop = this.stops[this.stops.length - 1]
|
||||
const newSpeed = lastStop.speed + 10
|
||||
|
||||
this.stops.push({
|
||||
speed: newSpeed,
|
||||
color: '#ff0000'
|
||||
})
|
||||
|
||||
// Sort by speed
|
||||
this.stops.sort((a, b) => a.speed - b.speed)
|
||||
|
||||
this.renderStops()
|
||||
this.updatePreview()
|
||||
}
|
||||
|
||||
removeStop(event) {
|
||||
const index = parseInt(event.target.dataset.index)
|
||||
|
||||
if (this.stops.length > 2) {
|
||||
this.stops.splice(index, 1)
|
||||
this.renderStops()
|
||||
this.updatePreview()
|
||||
}
|
||||
}
|
||||
|
||||
updatePreview() {
|
||||
if (!this.hasPreviewTarget) return
|
||||
|
||||
const gradient = this.stops.map((stop, index) => {
|
||||
const percentage = (index / (this.stops.length - 1)) * 100
|
||||
return `${stop.color} ${percentage}%`
|
||||
}).join(', ')
|
||||
|
||||
this.previewTarget.style.background = `linear-gradient(to right, ${gradient})`
|
||||
}
|
||||
|
||||
save() {
|
||||
const serialized = this.serializeColorStops()
|
||||
|
||||
// Dispatch event with the new color stops
|
||||
this.dispatch('save', {
|
||||
detail: { colorStops: serialized }
|
||||
})
|
||||
|
||||
this.close()
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.hasModalTarget) {
|
||||
const checkbox = this.modalTarget.querySelector('.modal-toggle')
|
||||
if (checkbox) {
|
||||
checkbox.checked = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetToDefault() {
|
||||
this.colorStopsValue = '0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300'
|
||||
this.loadColorStops()
|
||||
}
|
||||
}
|
||||
|
|
@ -31,7 +31,14 @@ export class RoutesLayer extends BaseLayer {
|
|||
'line-cap': 'round'
|
||||
},
|
||||
paint: {
|
||||
'line-color': '#f97316', // Orange color (more visible than blue)
|
||||
// Use color from feature properties if available (for speed-colored routes)
|
||||
// Otherwise fall back to default orange
|
||||
'line-color': [
|
||||
'case',
|
||||
['has', 'color'],
|
||||
['get', 'color'],
|
||||
'#f97316' // Default orange color
|
||||
],
|
||||
'line-width': 3,
|
||||
'line-opacity': 0.8
|
||||
}
|
||||
|
|
|
|||
140
app/javascript/maps_v2/utils/speed_colors.js
Normal file
140
app/javascript/maps_v2/utils/speed_colors.js
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
/**
|
||||
* Speed color utilities for route visualization
|
||||
* Provides speed calculation and color interpolation for route segments
|
||||
*/
|
||||
|
||||
// Default color stops for speed visualization
|
||||
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)
|
||||
]
|
||||
|
||||
/**
|
||||
* Encode color stops array to string format for storage
|
||||
* @param {Array} arr - Array of {speed, color} objects
|
||||
* @returns {string} Encoded string (e.g., "0:#00ff00|15:#00ffff")
|
||||
*/
|
||||
export function colorFormatEncode(arr) {
|
||||
return arr.map(item => `${item.speed}:${item.color}`).join('|')
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode color stops string to array format
|
||||
* @param {string} str - Encoded color stops string
|
||||
* @returns {Array} Array of {speed, color} objects
|
||||
*/
|
||||
export function colorFormatDecode(str) {
|
||||
return str.split('|').map(segment => {
|
||||
const [speed, color] = segment.split(':')
|
||||
return { speed: Number(speed), color }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hex color to RGB object
|
||||
* @param {string} hex - Hex color (e.g., "#ff0000")
|
||||
* @returns {Object} RGB object {r, g, b}
|
||||
*/
|
||||
function hexToRGB(hex) {
|
||||
const r = parseInt(hex.slice(1, 3), 16)
|
||||
const g = parseInt(hex.slice(3, 5), 16)
|
||||
const b = parseInt(hex.slice(5, 7), 16)
|
||||
return { r, g, b }
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate speed between two points
|
||||
* @param {Object} point1 - First point with lat, lon, timestamp
|
||||
* @param {Object} point2 - Second point with lat, lon, timestamp
|
||||
* @returns {number} Speed in km/h
|
||||
*/
|
||||
export function calculateSpeed(point1, point2) {
|
||||
if (!point1 || !point2 || !point1.timestamp || !point2.timestamp) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const distanceKm = haversineDistance(
|
||||
point1.latitude, point1.longitude,
|
||||
point2.latitude, point2.longitude
|
||||
)
|
||||
const timeDiffSeconds = point2.timestamp - point1.timestamp
|
||||
|
||||
// Handle edge cases
|
||||
if (timeDiffSeconds <= 0 || distanceKm <= 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const speedKmh = (distanceKm / timeDiffSeconds) * 3600
|
||||
|
||||
// Cap speed at reasonable maximum (150 km/h)
|
||||
const MAX_SPEED = 150
|
||||
return Math.min(speedKmh, MAX_SPEED)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate haversine distance between two points
|
||||
* @param {number} lat1 - First point latitude
|
||||
* @param {number} lon1 - First point longitude
|
||||
* @param {number} lat2 - Second point latitude
|
||||
* @param {number} lon2 - Second point longitude
|
||||
* @returns {number} Distance in kilometers
|
||||
*/
|
||||
function haversineDistance(lat1, lon1, lat2, lon2) {
|
||||
const R = 6371 // Earth's radius in kilometers
|
||||
const dLat = (lat2 - lat1) * Math.PI / 180
|
||||
const dLon = (lon2 - lon1) * Math.PI / 180
|
||||
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
|
||||
Math.sin(dLon / 2) * Math.sin(dLon / 2)
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
||||
return R * c
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for a given speed with interpolation
|
||||
* @param {number} speedKmh - Speed in km/h
|
||||
* @param {boolean} useSpeedColors - Whether to use speed-based coloring
|
||||
* @param {string} speedColorScale - Encoded color scale string
|
||||
* @returns {string} RGB color string (e.g., "rgb(255, 0, 0)")
|
||||
*/
|
||||
export function getSpeedColor(speedKmh, useSpeedColors, speedColorScale) {
|
||||
if (!useSpeedColors) {
|
||||
return '#f97316' // Default orange color
|
||||
}
|
||||
|
||||
let colorStops
|
||||
|
||||
try {
|
||||
colorStops = colorFormatDecode(speedColorScale).map(stop => ({
|
||||
...stop,
|
||||
rgb: hexToRGB(stop.color)
|
||||
}))
|
||||
} catch (error) {
|
||||
// If user has given invalid values, use fallback
|
||||
colorStops = colorStopsFallback.map(stop => ({
|
||||
...stop,
|
||||
rgb: hexToRGB(stop.color)
|
||||
}))
|
||||
}
|
||||
|
||||
// Find the appropriate color segment and interpolate
|
||||
for (let i = 1; i < colorStops.length; i++) {
|
||||
if (speedKmh <= colorStops[i].speed) {
|
||||
const ratio = (speedKmh - colorStops[i-1].speed) / (colorStops[i].speed - colorStops[i-1].speed)
|
||||
const color1 = colorStops[i-1].rgb
|
||||
const color2 = colorStops[i].rgb
|
||||
|
||||
const r = Math.round(color1.r + (color2.r - color1.r) * ratio)
|
||||
const g = Math.round(color1.g + (color2.g - color1.g) * ratio)
|
||||
const b = Math.round(color1.b + (color2.b - color1.b) * ratio)
|
||||
|
||||
return `rgb(${r}, ${g}, ${b})`
|
||||
}
|
||||
}
|
||||
|
||||
// If speed exceeds all stops, return the last color
|
||||
return colorStops[colorStops.length - 1].color
|
||||
}
|
||||
|
|
@ -106,6 +106,32 @@
|
|||
<p class="text-sm text-base-content/60 ml-14">Show connected route lines</p>
|
||||
</div>
|
||||
|
||||
<!-- Speed-Colored Routes Options (conditionally shown) -->
|
||||
<div class="ml-14 space-y-3" data-maps-v2-target="routesOptions" style="display: none;">
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer py-2">
|
||||
<span class="label-text text-sm">Color by speed</span>
|
||||
<input type="checkbox"
|
||||
class="toggle toggle-sm toggle-primary"
|
||||
data-maps-v2-target="speedColoredToggle"
|
||||
data-action="change->maps-v2#toggleSpeedColoredRoutes" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Speed Color Scale Editor (shown when speed colors enabled) -->
|
||||
<div class="hidden" data-maps-v2-target="speedColorScaleContainer">
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline w-full"
|
||||
data-action="click->maps-v2#openSpeedColorEditor">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
|
||||
</svg>
|
||||
Edit Color Gradient
|
||||
</button>
|
||||
<input type="hidden" data-maps-v2-target="speedColorScaleInput" value="" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider my-2"></div>
|
||||
|
||||
<!-- Heatmap Layer -->
|
||||
|
|
|
|||
Loading…
Reference in a new issue