mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-09 08:47:11 -05:00
287 lines
8.9 KiB
JavaScript
287 lines
8.9 KiB
JavaScript
import L from "leaflet";
|
|
import "leaflet.heat";
|
|
import { createAllMapLayers } from "../maps/layers";
|
|
import BaseController from "./base_controller";
|
|
|
|
export default class extends BaseController {
|
|
static targets = ["map", "loading", "heatmapBtn", "pointsBtn"];
|
|
|
|
connect() {
|
|
super.connect();
|
|
console.log("StatPage controller connected");
|
|
|
|
// Get data attributes from the element (will be passed from the view)
|
|
this.year = parseInt(this.element.dataset.year || new Date().getFullYear());
|
|
this.month = parseInt(this.element.dataset.month || new Date().getMonth() + 1);
|
|
this.apiKey = this.element.dataset.apiKey;
|
|
this.selfHosted = this.element.dataset.selfHosted || this.selfHostedValue;
|
|
|
|
console.log(`Loading data for ${this.month}/${this.year} with API key: ${this.apiKey ? 'present' : 'missing'}`);
|
|
|
|
// Initialize map after a short delay to ensure container is ready
|
|
setTimeout(() => {
|
|
this.initializeMap();
|
|
}, 100);
|
|
}
|
|
|
|
disconnect() {
|
|
if (this.map) {
|
|
this.map.remove();
|
|
}
|
|
console.log("StatPage controller disconnected");
|
|
}
|
|
|
|
initializeMap() {
|
|
if (!this.mapTarget) {
|
|
console.error("Map target not found");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Initialize Leaflet map
|
|
this.map = L.map(this.mapTarget, {
|
|
zoomControl: true,
|
|
scrollWheelZoom: true,
|
|
doubleClickZoom: true,
|
|
boxZoom: false,
|
|
keyboard: false,
|
|
dragging: true,
|
|
touchZoom: true
|
|
}).setView([52.520008, 13.404954], 10); // Default to Berlin
|
|
|
|
// Add dynamic tile layer based on self-hosted setting
|
|
this.addMapLayers();
|
|
|
|
// Add small scale control
|
|
L.control.scale({
|
|
position: 'bottomright',
|
|
maxWidth: 100,
|
|
imperial: true,
|
|
metric: true
|
|
}).addTo(this.map);
|
|
|
|
// Initialize layers
|
|
this.markersLayer = L.layerGroup(); // Don't add to map initially
|
|
this.heatmapLayer = null;
|
|
|
|
// Load data for this month
|
|
this.loadMonthData();
|
|
|
|
} catch (error) {
|
|
console.error("Error initializing map:", error);
|
|
this.showError("Failed to initialize map");
|
|
}
|
|
}
|
|
|
|
async loadMonthData() {
|
|
try {
|
|
// Show loading
|
|
this.showLoading(true);
|
|
|
|
// Calculate date range for the month
|
|
const startDate = `${this.year}-${this.month.toString().padStart(2, '0')}-01T00:00:00`;
|
|
const lastDay = new Date(this.year, this.month, 0).getDate();
|
|
const endDate = `${this.year}-${this.month.toString().padStart(2, '0')}-${lastDay}T23:59:59`;
|
|
|
|
console.log(`Fetching points from ${startDate} to ${endDate}`);
|
|
|
|
// Fetch points data for the month using Authorization header
|
|
const response = await fetch(`/api/v1/points?start_at=${encodeURIComponent(startDate)}&end_at=${encodeURIComponent(endDate)}&per_page=1000`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${this.apiKey}`
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
console.error(`API request failed with status: ${response.status}`);
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log(`Received ${Array.isArray(data) ? data.length : 0} points from API`);
|
|
|
|
if (Array.isArray(data) && data.length > 0) {
|
|
this.processPointsData(data);
|
|
} else {
|
|
console.log("No points data available for this month");
|
|
this.showNoData();
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error("Error loading month data:", error);
|
|
this.showError("Failed to load location data");
|
|
// Don't fallback to mock data - show the error instead
|
|
} finally {
|
|
this.showLoading(false);
|
|
}
|
|
}
|
|
|
|
processPointsData(points) {
|
|
console.log(`Processing ${points.length} points for ${this.month}/${this.year}`);
|
|
|
|
// Clear existing markers
|
|
this.markersLayer.clearLayers();
|
|
|
|
// Convert points to markers (API returns latitude/longitude as strings)
|
|
const markers = points.map(point => {
|
|
const lat = parseFloat(point.latitude);
|
|
const lng = parseFloat(point.longitude);
|
|
|
|
return L.circleMarker([lat, lng], {
|
|
radius: 3,
|
|
fillColor: '#570df8',
|
|
color: '#570df8',
|
|
weight: 1,
|
|
opacity: 0.8,
|
|
fillOpacity: 0.6
|
|
});
|
|
});
|
|
|
|
// Add markers to layer (but don't add to map yet)
|
|
markers.forEach(marker => {
|
|
this.markersLayer.addLayer(marker);
|
|
});
|
|
|
|
// Prepare data for heatmap (convert strings to numbers)
|
|
this.heatmapData = points.map(point => [
|
|
parseFloat(point.latitude),
|
|
parseFloat(point.longitude),
|
|
0.5
|
|
]);
|
|
|
|
// Show heatmap by default
|
|
if (this.heatmapData.length > 0) {
|
|
this.heatmapLayer = L.heatLayer(this.heatmapData, {
|
|
radius: 25,
|
|
blur: 15,
|
|
maxZoom: 17,
|
|
max: 1.0
|
|
}).addTo(this.map);
|
|
|
|
// Set button states
|
|
this.heatmapBtnTarget.classList.add('btn-active');
|
|
this.pointsBtnTarget.classList.remove('btn-active');
|
|
}
|
|
|
|
// Fit map to show all points
|
|
if (points.length > 0) {
|
|
const group = new L.featureGroup(markers);
|
|
this.map.fitBounds(group.getBounds().pad(0.1));
|
|
}
|
|
|
|
console.log("Points processed successfully");
|
|
}
|
|
|
|
toggleHeatmap() {
|
|
if (!this.heatmapData || this.heatmapData.length === 0) {
|
|
console.warn("No heatmap data available");
|
|
return;
|
|
}
|
|
|
|
if (this.heatmapLayer && this.map.hasLayer(this.heatmapLayer)) {
|
|
// Remove heatmap
|
|
this.map.removeLayer(this.heatmapLayer);
|
|
this.heatmapLayer = null;
|
|
this.heatmapBtnTarget.classList.remove('btn-active');
|
|
|
|
// Show points
|
|
if (!this.map.hasLayer(this.markersLayer)) {
|
|
this.map.addLayer(this.markersLayer);
|
|
this.pointsBtnTarget.classList.add('btn-active');
|
|
}
|
|
} else {
|
|
// Add heatmap
|
|
this.heatmapLayer = L.heatLayer(this.heatmapData, {
|
|
radius: 25,
|
|
blur: 15,
|
|
maxZoom: 17,
|
|
max: 1.0
|
|
}).addTo(this.map);
|
|
|
|
this.heatmapBtnTarget.classList.add('btn-active');
|
|
|
|
// Hide points
|
|
if (this.map.hasLayer(this.markersLayer)) {
|
|
this.map.removeLayer(this.markersLayer);
|
|
this.pointsBtnTarget.classList.remove('btn-active');
|
|
}
|
|
}
|
|
}
|
|
|
|
togglePoints() {
|
|
if (this.map.hasLayer(this.markersLayer)) {
|
|
// Remove points
|
|
this.map.removeLayer(this.markersLayer);
|
|
this.pointsBtnTarget.classList.remove('btn-active');
|
|
} else {
|
|
// Add points
|
|
this.map.addLayer(this.markersLayer);
|
|
this.pointsBtnTarget.classList.add('btn-active');
|
|
|
|
// Remove heatmap if active
|
|
if (this.heatmapLayer && this.map.hasLayer(this.heatmapLayer)) {
|
|
this.map.removeLayer(this.heatmapLayer);
|
|
this.heatmapBtnTarget.classList.remove('btn-active');
|
|
}
|
|
}
|
|
}
|
|
|
|
showLoading(show) {
|
|
if (this.hasLoadingTarget) {
|
|
this.loadingTarget.style.display = show ? 'flex' : 'none';
|
|
}
|
|
}
|
|
|
|
showError(message) {
|
|
console.error(message);
|
|
if (this.hasLoadingTarget) {
|
|
this.loadingTarget.innerHTML = `
|
|
<div class="alert alert-error">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
|
<span>${message}</span>
|
|
</div>
|
|
`;
|
|
this.loadingTarget.style.display = 'flex';
|
|
}
|
|
}
|
|
|
|
showNoData() {
|
|
console.log("No data available for this month");
|
|
if (this.hasLoadingTarget) {
|
|
this.loadingTarget.innerHTML = `
|
|
<div class="alert alert-info">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
|
<span>No location data available for ${new Date(this.year, this.month - 1).toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}</span>
|
|
</div>
|
|
`;
|
|
this.loadingTarget.style.display = 'flex';
|
|
}
|
|
}
|
|
|
|
addMapLayers() {
|
|
try {
|
|
// Use appropriate default layer based on self-hosted mode
|
|
const selectedLayerName = this.selfHosted === "true" ? "OpenStreetMap" : "Light";
|
|
const maps = createAllMapLayers(this.map, selectedLayerName, this.selfHosted, 'dark');
|
|
|
|
// If no layers were created, fall back to OSM
|
|
if (Object.keys(maps).length === 0) {
|
|
console.warn('No map layers available, falling back to OSM');
|
|
this.addFallbackOSMLayer();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error creating map layers:', error);
|
|
console.log('Falling back to OSM tile layer');
|
|
this.addFallbackOSMLayer();
|
|
}
|
|
}
|
|
|
|
addFallbackOSMLayer() {
|
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
maxZoom: 19,
|
|
attribution: '© OpenStreetMap contributors'
|
|
}).addTo(this.map);
|
|
}
|
|
}
|