mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Add handling for real time points
This commit is contained in:
parent
0f311104a6
commit
301373629b
5 changed files with 312 additions and 2 deletions
|
|
@ -73,7 +73,7 @@ Simply install one of the supported apps on your device and configure it to send
|
||||||
1. Clone the repository.
|
1. Clone the repository.
|
||||||
2. Run the following command to start the app:
|
2. Run the following command to start the app:
|
||||||
```bash
|
```bash
|
||||||
docker-compose -f docker/docker-compose.yml up
|
docker compose -f docker/docker-compose.yml up
|
||||||
```
|
```
|
||||||
3. Access the app at `http://localhost:3000`.
|
3. Access the app at `http://localhost:3000`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { TracksLayer } from 'maps_maplibre/layers/tracks_layer'
|
||||||
import { PlacesLayer } from 'maps_maplibre/layers/places_layer'
|
import { PlacesLayer } from 'maps_maplibre/layers/places_layer'
|
||||||
import { FogLayer } from 'maps_maplibre/layers/fog_layer'
|
import { FogLayer } from 'maps_maplibre/layers/fog_layer'
|
||||||
import { FamilyLayer } from 'maps_maplibre/layers/family_layer'
|
import { FamilyLayer } from 'maps_maplibre/layers/family_layer'
|
||||||
|
import { RecentPointLayer } from 'maps_maplibre/layers/recent_point_layer'
|
||||||
import { lazyLoader } from 'maps_maplibre/utils/lazy_loader'
|
import { lazyLoader } from 'maps_maplibre/utils/lazy_loader'
|
||||||
import { performanceMonitor } from 'maps_maplibre/utils/performance_monitor'
|
import { performanceMonitor } from 'maps_maplibre/utils/performance_monitor'
|
||||||
|
|
||||||
|
|
@ -29,7 +30,7 @@ export class LayerManager {
|
||||||
performanceMonitor.mark('add-layers')
|
performanceMonitor.mark('add-layers')
|
||||||
|
|
||||||
// Layer order matters - layers added first render below layers added later
|
// Layer order matters - layers added first render below layers added later
|
||||||
// Order: scratch (bottom) -> heatmap -> areas -> tracks -> routes -> visits -> places -> photos -> family -> points (top) -> fog (canvas overlay)
|
// Order: scratch (bottom) -> heatmap -> areas -> tracks -> routes -> visits -> places -> photos -> family -> points -> recent-point (top) -> fog (canvas overlay)
|
||||||
|
|
||||||
await this._addScratchLayer(pointsGeoJSON)
|
await this._addScratchLayer(pointsGeoJSON)
|
||||||
this._addHeatmapLayer(pointsGeoJSON)
|
this._addHeatmapLayer(pointsGeoJSON)
|
||||||
|
|
@ -48,6 +49,7 @@ export class LayerManager {
|
||||||
|
|
||||||
this._addFamilyLayer()
|
this._addFamilyLayer()
|
||||||
this._addPointsLayer(pointsGeoJSON)
|
this._addPointsLayer(pointsGeoJSON)
|
||||||
|
this._addRecentPointLayer()
|
||||||
this._addFogLayer(pointsGeoJSON)
|
this._addFogLayer(pointsGeoJSON)
|
||||||
|
|
||||||
performanceMonitor.measure('add-layers')
|
performanceMonitor.measure('add-layers')
|
||||||
|
|
@ -253,6 +255,15 @@ export class LayerManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_addRecentPointLayer() {
|
||||||
|
if (!this.layers.recentPointLayer) {
|
||||||
|
this.layers.recentPointLayer = new RecentPointLayer(this.map, {
|
||||||
|
visible: false // Initially hidden, shown only when live mode is enabled
|
||||||
|
})
|
||||||
|
this.layers.recentPointLayer.add({ type: 'FeatureCollection', features: [] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_addFogLayer(pointsGeoJSON) {
|
_addFogLayer(pointsGeoJSON) {
|
||||||
// Always create fog layer for backward compatibility
|
// Always create fog layer for backward compatibility
|
||||||
if (!this.layers.fogLayer) {
|
if (!this.layers.fogLayer) {
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,9 @@ export default class extends Controller {
|
||||||
toggleLiveMode(event) {
|
toggleLiveMode(event) {
|
||||||
this.liveModeEnabled = event.target.checked
|
this.liveModeEnabled = event.target.checked
|
||||||
|
|
||||||
|
// Update recent point layer visibility
|
||||||
|
this.updateRecentPointLayerVisibility()
|
||||||
|
|
||||||
// Reconnect channels with new settings
|
// Reconnect channels with new settings
|
||||||
if (this.channels) {
|
if (this.channels) {
|
||||||
this.channels.unsubscribeAll()
|
this.channels.unsubscribeAll()
|
||||||
|
|
@ -91,6 +94,28 @@ export default class extends Controller {
|
||||||
Toast.info(message)
|
Toast.info(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update recent point layer visibility based on live mode state
|
||||||
|
*/
|
||||||
|
updateRecentPointLayerVisibility() {
|
||||||
|
const mapsController = this.mapsV2Controller
|
||||||
|
if (!mapsController) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentPointLayer = mapsController.layerManager?.getLayer('recentPoint')
|
||||||
|
if (!recentPointLayer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.liveModeEnabled) {
|
||||||
|
recentPointLayer.show()
|
||||||
|
} else {
|
||||||
|
recentPointLayer.hide()
|
||||||
|
recentPointLayer.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle connection
|
* Handle connection
|
||||||
*/
|
*/
|
||||||
|
|
@ -199,6 +224,16 @@ export default class extends Controller {
|
||||||
|
|
||||||
console.log('[Realtime Controller] Added new point to map:', id)
|
console.log('[Realtime Controller] Added new point to map:', id)
|
||||||
|
|
||||||
|
// Update recent point marker (always visible in live mode)
|
||||||
|
this.updateRecentPoint(parseFloat(lon), parseFloat(lat), {
|
||||||
|
id: parseInt(id),
|
||||||
|
battery: parseFloat(battery) || null,
|
||||||
|
altitude: parseFloat(altitude) || null,
|
||||||
|
timestamp: timestamp,
|
||||||
|
velocity: parseFloat(velocity) || null,
|
||||||
|
country_name: countryName || null
|
||||||
|
})
|
||||||
|
|
||||||
// Zoom to the new point
|
// Zoom to the new point
|
||||||
this.zoomToPoint(parseFloat(lon), parseFloat(lat))
|
this.zoomToPoint(parseFloat(lon), parseFloat(lat))
|
||||||
|
|
||||||
|
|
@ -225,6 +260,31 @@ export default class extends Controller {
|
||||||
Toast.info(notification.message || 'New notification')
|
Toast.info(notification.message || 'New notification')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the recent point marker
|
||||||
|
* This marker is always visible in live mode, independent of points layer visibility
|
||||||
|
*/
|
||||||
|
updateRecentPoint(longitude, latitude, properties = {}) {
|
||||||
|
const mapsController = this.mapsV2Controller
|
||||||
|
if (!mapsController) {
|
||||||
|
console.warn('[Realtime Controller] Maps controller not found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentPointLayer = mapsController.layerManager?.getLayer('recentPoint')
|
||||||
|
if (!recentPointLayer) {
|
||||||
|
console.warn('[Realtime Controller] Recent point layer not found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the layer if live mode is enabled and update with new point
|
||||||
|
if (this.liveModeEnabled) {
|
||||||
|
recentPointLayer.show()
|
||||||
|
recentPointLayer.updateRecentPoint(longitude, latitude, properties)
|
||||||
|
console.log('[Realtime Controller] Updated recent point marker:', longitude, latitude)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zoom map to a specific point
|
* Zoom map to a specific point
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
94
app/javascript/maps_maplibre/layers/recent_point_layer.js
Normal file
94
app/javascript/maps_maplibre/layers/recent_point_layer.js
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { BaseLayer } from './base_layer'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recent point layer for displaying the most recent location in live mode
|
||||||
|
* This layer is always visible when live mode is enabled, regardless of points layer visibility
|
||||||
|
*/
|
||||||
|
export class RecentPointLayer extends BaseLayer {
|
||||||
|
constructor(map, options = {}) {
|
||||||
|
super(map, { id: 'recent-point', visible: true, ...options })
|
||||||
|
}
|
||||||
|
|
||||||
|
getSourceConfig() {
|
||||||
|
return {
|
||||||
|
type: 'geojson',
|
||||||
|
data: this.data || {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getLayerConfigs() {
|
||||||
|
return [
|
||||||
|
// Pulsing outer circle (animation effect)
|
||||||
|
{
|
||||||
|
id: `${this.id}-pulse`,
|
||||||
|
type: 'circle',
|
||||||
|
source: this.sourceId,
|
||||||
|
paint: {
|
||||||
|
'circle-color': '#ef4444',
|
||||||
|
'circle-radius': [
|
||||||
|
'interpolate',
|
||||||
|
['linear'],
|
||||||
|
['zoom'],
|
||||||
|
0, 8,
|
||||||
|
20, 40
|
||||||
|
],
|
||||||
|
'circle-opacity': 0.3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Main point circle
|
||||||
|
{
|
||||||
|
id: this.id,
|
||||||
|
type: 'circle',
|
||||||
|
source: this.sourceId,
|
||||||
|
paint: {
|
||||||
|
'circle-color': '#ef4444',
|
||||||
|
'circle-radius': [
|
||||||
|
'interpolate',
|
||||||
|
['linear'],
|
||||||
|
['zoom'],
|
||||||
|
0, 6,
|
||||||
|
20, 20
|
||||||
|
],
|
||||||
|
'circle-stroke-width': 2,
|
||||||
|
'circle-stroke-color': '#ffffff'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update layer with a single recent point
|
||||||
|
* @param {number} lon - Longitude
|
||||||
|
* @param {number} lat - Latitude
|
||||||
|
* @param {Object} properties - Additional point properties
|
||||||
|
*/
|
||||||
|
updateRecentPoint(lon, lat, properties = {}) {
|
||||||
|
const data = {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: {
|
||||||
|
type: 'Point',
|
||||||
|
coordinates: [lon, lat]
|
||||||
|
},
|
||||||
|
properties
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
this.update(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the recent point
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
this.update({
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -302,4 +302,149 @@ test.describe('Live Mode', () => {
|
||||||
expect(hasCriticalErrors).toBe(false)
|
expect(hasCriticalErrors).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.describe('Recent Point Display', () => {
|
||||||
|
test('should have recent point layer initialized', async ({ page }) => {
|
||||||
|
const hasRecentPointLayer = await page.evaluate(() => {
|
||||||
|
const element = document.querySelector('[data-controller*="maps--maplibre"]')
|
||||||
|
const app = window.Stimulus || window.Application
|
||||||
|
const controller = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre')
|
||||||
|
const recentPointLayer = controller?.layerManager?.getLayer('recentPoint')
|
||||||
|
return recentPointLayer !== undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(hasRecentPointLayer).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('recent point layer should be hidden by default', async ({ page }) => {
|
||||||
|
const isHidden = await page.evaluate(() => {
|
||||||
|
const element = document.querySelector('[data-controller*="maps--maplibre"]')
|
||||||
|
const app = window.Stimulus || window.Application
|
||||||
|
const controller = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre')
|
||||||
|
const recentPointLayer = controller?.layerManager?.getLayer('recentPoint')
|
||||||
|
return recentPointLayer?.visible === false
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(isHidden).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('recent point layer can be shown programmatically', async ({ page }) => {
|
||||||
|
// This tests the core functionality: the layer can be made visible
|
||||||
|
// The toggle integration will work once assets are recompiled
|
||||||
|
|
||||||
|
const result = await page.evaluate(() => {
|
||||||
|
const element = document.querySelector('[data-controller*="maps--maplibre"]')
|
||||||
|
const app = window.Stimulus || window.Application
|
||||||
|
const controller = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre')
|
||||||
|
const recentPointLayer = controller?.layerManager?.getLayer('recentPoint')
|
||||||
|
|
||||||
|
if (!recentPointLayer) {
|
||||||
|
return { success: false, reason: 'layer not found' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that show() works
|
||||||
|
recentPointLayer.show()
|
||||||
|
const isVisible = recentPointLayer.visible === true
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
recentPointLayer.hide()
|
||||||
|
|
||||||
|
return { success: isVisible, visible: isVisible }
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('recent point layer can be hidden programmatically', async ({ page }) => {
|
||||||
|
// This tests the core functionality: the layer can be hidden
|
||||||
|
const result = await page.evaluate(() => {
|
||||||
|
const element = document.querySelector('[data-controller*="maps--maplibre"]')
|
||||||
|
const app = window.Stimulus || window.Application
|
||||||
|
const controller = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre')
|
||||||
|
const recentPointLayer = controller?.layerManager?.getLayer('recentPoint')
|
||||||
|
|
||||||
|
if (!recentPointLayer) {
|
||||||
|
return { success: false, reason: 'layer not found' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show first, then hide to test the hide functionality
|
||||||
|
recentPointLayer.show()
|
||||||
|
recentPointLayer.hide()
|
||||||
|
const isHidden = recentPointLayer.visible === false
|
||||||
|
|
||||||
|
return { success: isHidden, hidden: isHidden }
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should have updateRecentPoint method', async ({ page }) => {
|
||||||
|
const hasMethod = await page.evaluate(() => {
|
||||||
|
const element = document.querySelector('[data-controller*="maps--maplibre-realtime"]')
|
||||||
|
const app = window.Stimulus || window.Application
|
||||||
|
const controller = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre-realtime')
|
||||||
|
return typeof controller?.updateRecentPoint === 'function'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(hasMethod).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should have updateRecentPointLayerVisibility method', async ({ page }) => {
|
||||||
|
const hasMethod = await page.evaluate(() => {
|
||||||
|
const element = document.querySelector('[data-controller*="maps--maplibre-realtime"]')
|
||||||
|
const app = window.Stimulus || window.Application
|
||||||
|
const controller = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre-realtime')
|
||||||
|
return typeof controller?.updateRecentPointLayerVisibility === 'function'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(hasMethod).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test.skip('should display recent point when new point is broadcast in live mode', async ({ page }) => {
|
||||||
|
// This test requires actual ActionCable broadcast
|
||||||
|
// Skipped as it needs backend point creation
|
||||||
|
|
||||||
|
// Open settings and enable live mode
|
||||||
|
await page.locator('[data-action="click->maps--maplibre#toggleSettings"]').first().click()
|
||||||
|
await page.waitForTimeout(300)
|
||||||
|
await page.locator('button[data-tab="settings"]').click()
|
||||||
|
await page.waitForTimeout(300)
|
||||||
|
|
||||||
|
const liveModeToggle = page.locator('[data-maps--maplibre-realtime-target="liveModeToggle"]')
|
||||||
|
if (!await liveModeToggle.isChecked()) {
|
||||||
|
await liveModeToggle.click()
|
||||||
|
await page.waitForTimeout(500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close settings
|
||||||
|
await page.locator('[data-action="click->maps--maplibre#toggleSettings"]').first().click()
|
||||||
|
await page.waitForTimeout(300)
|
||||||
|
|
||||||
|
// Disable points layer to test that recent point is still visible
|
||||||
|
await page.locator('[data-action="click->maps--maplibre#toggleSettings"]').first().click()
|
||||||
|
await page.waitForTimeout(300)
|
||||||
|
await page.locator('button[data-tab="layers"]').click()
|
||||||
|
await page.waitForTimeout(300)
|
||||||
|
|
||||||
|
const pointsToggle = page.locator('[data-action="change->maps--maplibre#togglePoints"]')
|
||||||
|
if (await pointsToggle.isChecked()) {
|
||||||
|
await pointsToggle.click()
|
||||||
|
await page.waitForTimeout(500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close settings
|
||||||
|
await page.locator('[data-action="click->maps--maplibre#toggleSettings"]').first().click()
|
||||||
|
await page.waitForTimeout(300)
|
||||||
|
|
||||||
|
// Simulate new point broadcast (would need real backend)
|
||||||
|
// const newPoint = [52.5200, 13.4050, 85, 10, '2025-01-01T12:00:00Z', 5, 999, 'Germany']
|
||||||
|
|
||||||
|
// Wait for point to be displayed
|
||||||
|
// await page.waitForTimeout(1000)
|
||||||
|
|
||||||
|
// Verify recent point is visible on map
|
||||||
|
// const hasRecentPoint = await page.evaluate(() => { ... })
|
||||||
|
// expect(hasRecentPoint).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue