mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 01:01:39 -05:00
* Implement some performance improvements and caching for various features. * Fix failing tests * Implement routes behaviour in map v2 to match map v1 * Fix route highlighting * Add fallbacks when retrieving full route features to handle cases where source data access methods vary. * Fix some e2e tests
250 lines
7.1 KiB
JavaScript
250 lines
7.1 KiB
JavaScript
import { pointsToGeoJSON } from 'maps_maplibre/utils/geojson_transformers'
|
|
import { RoutesLayer } from 'maps_maplibre/layers/routes_layer'
|
|
import { createCircle } from 'maps_maplibre/utils/geometry'
|
|
import { performanceMonitor } from 'maps_maplibre/utils/performance_monitor'
|
|
|
|
/**
|
|
* Handles loading and transforming data from API
|
|
*/
|
|
export class DataLoader {
|
|
constructor(api, apiKey, settings = {}) {
|
|
this.api = api
|
|
this.apiKey = apiKey
|
|
this.settings = settings
|
|
}
|
|
|
|
/**
|
|
* Update settings (called when user changes settings)
|
|
*/
|
|
updateSettings(settings) {
|
|
this.settings = settings
|
|
}
|
|
|
|
/**
|
|
* Fetch all map data (points, visits, photos, areas, tracks)
|
|
*/
|
|
async fetchMapData(startDate, endDate, onProgress) {
|
|
const data = {}
|
|
|
|
// Fetch points
|
|
performanceMonitor.mark('fetch-points')
|
|
data.points = await this.api.fetchAllPoints({
|
|
start_at: startDate,
|
|
end_at: endDate,
|
|
onProgress: onProgress
|
|
})
|
|
performanceMonitor.measure('fetch-points')
|
|
|
|
// Transform points to GeoJSON
|
|
performanceMonitor.mark('transform-geojson')
|
|
data.pointsGeoJSON = pointsToGeoJSON(data.points)
|
|
data.routesGeoJSON = RoutesLayer.pointsToRoutes(data.points, {
|
|
distanceThresholdMeters: this.settings.metersBetweenRoutes || 500,
|
|
timeThresholdMinutes: this.settings.minutesBetweenRoutes || 60
|
|
})
|
|
performanceMonitor.measure('transform-geojson')
|
|
|
|
// Fetch visits
|
|
try {
|
|
data.visits = await this.api.fetchVisits({
|
|
start_at: startDate,
|
|
end_at: endDate
|
|
})
|
|
} catch (error) {
|
|
console.warn('Failed to fetch visits:', error)
|
|
data.visits = []
|
|
}
|
|
data.visitsGeoJSON = this.visitsToGeoJSON(data.visits)
|
|
|
|
// Fetch photos - only if photos layer is enabled and integration is configured
|
|
// Skip API call if photos are disabled to avoid blocking on failed integrations
|
|
if (this.settings.photosEnabled) {
|
|
try {
|
|
console.log('[Photos] Fetching photos from:', startDate, 'to', endDate)
|
|
// Use Promise.race to enforce a client-side timeout
|
|
const photosPromise = this.api.fetchPhotos({
|
|
start_at: startDate,
|
|
end_at: endDate
|
|
})
|
|
const timeoutPromise = new Promise((_, reject) =>
|
|
setTimeout(() => reject(new Error('Photo fetch timeout')), 15000) // 15 second timeout
|
|
)
|
|
|
|
data.photos = await Promise.race([photosPromise, timeoutPromise])
|
|
console.log('[Photos] Fetched photos:', data.photos.length, 'photos')
|
|
console.log('[Photos] Sample photo:', data.photos[0])
|
|
} catch (error) {
|
|
console.warn('[Photos] Failed to fetch photos (non-blocking):', error.message)
|
|
data.photos = []
|
|
}
|
|
} else {
|
|
console.log('[Photos] Photos layer disabled, skipping fetch')
|
|
data.photos = []
|
|
}
|
|
data.photosGeoJSON = this.photosToGeoJSON(data.photos)
|
|
console.log('[Photos] Converted to GeoJSON:', data.photosGeoJSON.features.length, 'features')
|
|
if (data.photosGeoJSON.features.length > 0) {
|
|
console.log('[Photos] Sample feature:', data.photosGeoJSON.features[0])
|
|
}
|
|
|
|
// Fetch areas
|
|
try {
|
|
data.areas = await this.api.fetchAreas()
|
|
} catch (error) {
|
|
console.warn('Failed to fetch areas:', error)
|
|
data.areas = []
|
|
}
|
|
data.areasGeoJSON = this.areasToGeoJSON(data.areas)
|
|
|
|
// Fetch places (no date filtering)
|
|
try {
|
|
data.places = await this.api.fetchPlaces()
|
|
} catch (error) {
|
|
console.warn('Failed to fetch places:', error)
|
|
data.places = []
|
|
}
|
|
data.placesGeoJSON = this.placesToGeoJSON(data.places)
|
|
|
|
// Tracks - DISABLED: Backend API not yet implemented
|
|
// TODO: Re-enable when /api/v1/tracks endpoint is created
|
|
data.tracks = []
|
|
data.tracksGeoJSON = this.tracksToGeoJSON(data.tracks)
|
|
|
|
return data
|
|
}
|
|
|
|
/**
|
|
* Convert visits to GeoJSON
|
|
*/
|
|
visitsToGeoJSON(visits) {
|
|
return {
|
|
type: 'FeatureCollection',
|
|
features: visits.map(visit => ({
|
|
type: 'Feature',
|
|
geometry: {
|
|
type: 'Point',
|
|
coordinates: [visit.place.longitude, visit.place.latitude]
|
|
},
|
|
properties: {
|
|
id: visit.id,
|
|
name: visit.name,
|
|
place_name: visit.place?.name,
|
|
status: visit.status,
|
|
started_at: visit.started_at,
|
|
ended_at: visit.ended_at,
|
|
duration: visit.duration
|
|
}
|
|
}))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert photos to GeoJSON
|
|
*/
|
|
photosToGeoJSON(photos) {
|
|
return {
|
|
type: 'FeatureCollection',
|
|
features: photos.map(photo => {
|
|
// Construct thumbnail URL
|
|
const thumbnailUrl = `/api/v1/photos/${photo.id}/thumbnail.jpg?api_key=${this.apiKey}&source=${photo.source}`
|
|
|
|
return {
|
|
type: 'Feature',
|
|
geometry: {
|
|
type: 'Point',
|
|
coordinates: [photo.longitude, photo.latitude]
|
|
},
|
|
properties: {
|
|
id: photo.id,
|
|
thumbnail_url: thumbnailUrl,
|
|
taken_at: photo.localDateTime,
|
|
filename: photo.originalFileName,
|
|
city: photo.city,
|
|
state: photo.state,
|
|
country: photo.country,
|
|
type: photo.type,
|
|
source: photo.source
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert places to GeoJSON
|
|
*/
|
|
placesToGeoJSON(places) {
|
|
return {
|
|
type: 'FeatureCollection',
|
|
features: places.map(place => ({
|
|
type: 'Feature',
|
|
geometry: {
|
|
type: 'Point',
|
|
coordinates: [place.longitude, place.latitude]
|
|
},
|
|
properties: {
|
|
id: place.id,
|
|
name: place.name,
|
|
latitude: place.latitude,
|
|
longitude: place.longitude,
|
|
note: place.note,
|
|
// Stringify tags for MapLibre GL JS compatibility
|
|
tags: JSON.stringify(place.tags || []),
|
|
// Use first tag's color if available
|
|
color: place.tags?.[0]?.color || '#6366f1'
|
|
}
|
|
}))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert areas to GeoJSON
|
|
* Backend returns circular areas with latitude, longitude, radius
|
|
*/
|
|
areasToGeoJSON(areas) {
|
|
return {
|
|
type: 'FeatureCollection',
|
|
features: areas.map(area => {
|
|
// Create circle polygon from center and radius
|
|
// Parse as floats since API returns strings
|
|
const center = [parseFloat(area.longitude), parseFloat(area.latitude)]
|
|
const coordinates = createCircle(center, area.radius)
|
|
|
|
return {
|
|
type: 'Feature',
|
|
geometry: {
|
|
type: 'Polygon',
|
|
coordinates: [coordinates]
|
|
},
|
|
properties: {
|
|
id: area.id,
|
|
name: area.name,
|
|
color: area.color || '#ef4444',
|
|
radius: area.radius
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert tracks to GeoJSON
|
|
*/
|
|
tracksToGeoJSON(tracks) {
|
|
return {
|
|
type: 'FeatureCollection',
|
|
features: tracks.map(track => ({
|
|
type: 'Feature',
|
|
geometry: {
|
|
type: 'LineString',
|
|
coordinates: track.coordinates
|
|
},
|
|
properties: {
|
|
id: track.id,
|
|
name: track.name,
|
|
color: track.color || '#8b5cf6'
|
|
}
|
|
}))
|
|
}
|
|
}
|
|
}
|