Add handling for real time points

This commit is contained in:
Eugene Burmakin 2025-12-06 14:20:55 +01:00
parent 0f311104a6
commit 301373629b
5 changed files with 312 additions and 2 deletions

View file

@ -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`.

View file

@ -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) {

View file

@ -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
*/ */

View 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: []
})
}
}

View file

@ -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)
})
})
}) })