mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-09 08:47:11 -05:00
* fix: move foreman to global gems to fix startup crash (#1971) * Update exporting code to stream points data to file in batches to red… (#1980) * Update exporting code to stream points data to file in batches to reduce memory usage * Update changelog * Update changelog * Feature/maplibre frontend (#1953) * Add a plan to use MapLibre GL JS for the frontend map rendering, replacing Leaflet * Implement phase 1 * Phases 1-3 + part of 4 * Fix e2e tests * Phase 6 * Implement fog of war * Phase 7 * Next step: fix specs, phase 7 done * Use our own map tiles * Extract v2 map logic to separate manager classes * Update settings panel on v2 map * Update v2 e2e tests structure * Reimplement location search in maps v2 * Update speed routes * Implement visits and places creation in v2 * Fix last failing test * Implement visits merging * Fix a routes e2e test and simplify the routes layer styling. * Extract js to modules from maps_v2_controller.js * Implement area creation * Fix spec problem * Fix some e2e tests * Implement live mode in v2 map * Update icons and panel * Extract some styles * Remove unused file * Start adding dark theme to popups on MapLibre maps * Make popups respect dark theme * Move v2 maps to maplibre namespace * Update v2 references to maplibre * Put place, area and visit info into side panel * Update API to use safe settings config method * Fix specs * Fix method name to config in SafeSettings and update usages accordingly * Add missing public files * Add handling for real time points * Fix remembering enabled/disabled layers of the v2 map * Fix lots of e2e tests * Add settings to select map version * Use maps/v2 as main path for MapLibre maps * Update routing * Update live mode * Update maplibre controller * Update changelog * Remove some console.log statements * Pull only necessary data for map v2 points * Feature/raw data archive (#2009) * 0.36.2 (#2007) * fix: move foreman to global gems to fix startup crash (#1971) * Update exporting code to stream points data to file in batches to red… (#1980) * Update exporting code to stream points data to file in batches to reduce memory usage * Update changelog * Update changelog * Feature/maplibre frontend (#1953) * Add a plan to use MapLibre GL JS for the frontend map rendering, replacing Leaflet * Implement phase 1 * Phases 1-3 + part of 4 * Fix e2e tests * Phase 6 * Implement fog of war * Phase 7 * Next step: fix specs, phase 7 done * Use our own map tiles * Extract v2 map logic to separate manager classes * Update settings panel on v2 map * Update v2 e2e tests structure * Reimplement location search in maps v2 * Update speed routes * Implement visits and places creation in v2 * Fix last failing test * Implement visits merging * Fix a routes e2e test and simplify the routes layer styling. * Extract js to modules from maps_v2_controller.js * Implement area creation * Fix spec problem * Fix some e2e tests * Implement live mode in v2 map * Update icons and panel * Extract some styles * Remove unused file * Start adding dark theme to popups on MapLibre maps * Make popups respect dark theme * Move v2 maps to maplibre namespace * Update v2 references to maplibre * Put place, area and visit info into side panel * Update API to use safe settings config method * Fix specs * Fix method name to config in SafeSettings and update usages accordingly * Add missing public files * Add handling for real time points * Fix remembering enabled/disabled layers of the v2 map * Fix lots of e2e tests * Add settings to select map version * Use maps/v2 as main path for MapLibre maps * Update routing * Update live mode * Update maplibre controller * Update changelog * Remove some console.log statements --------- Co-authored-by: Robin Tuszik <mail@robin.gg> * Remove esbuild scripts from package.json * Remove sideEffects field from package.json * Raw data archivation * Add tests * Fix tests * Fix tests * Update ExceptionReporter * Add schedule to run raw data archival job monthly * Change file structure for raw data archival feature * Update changelog and version for raw data archival feature --------- Co-authored-by: Robin Tuszik <mail@robin.gg> * Set raw_data to an empty hash instead of nil when archiving * Fix storage configuration and file extraction * Consider MIN_MINUTES_SPENT_IN_CITY during stats calculation (#2018) * Consider MIN_MINUTES_SPENT_IN_CITY during stats calculation * Remove raw data from visited cities api endpoint * Use user timezone to show dates on maps (#2020) * Fix/pre epoch time (#2019) * Use user timezone to show dates on maps * Limit timestamps to valid range to prevent database errors when users enter pre-epoch dates. * Limit timestamps to valid range to prevent database errors when users enter pre-epoch dates. * Fix tests failing due to new index on stats table * Fix failing specs * Update redis client configuration to support unix socket connection * Update changelog * Fix kml kmz import issues (#2023) * Fix kml kmz import issues * Refactor KML importer to improve readability and maintainability * Implement moving points in map v2 and fix route rendering logic to ma… (#2027) * Implement moving points in map v2 and fix route rendering logic to match map v1. * Fix route spec * fix(maplibre): update date format to ISO 8601 (#2029) * Add verification step to raw data archival process (#2028) * Add verification step to raw data archival process * Add actual verification of raw data archives after creation, and only clear raw_data for verified archives. * Fix failing specs * Eliminate zip-bomb risk * Fix potential memory leak in js * Return .keep files * Use Toast instead of alert for notifications * Add help section to navbar dropdown * Update changelog * Remove raw_data_archival_job * Ensure file is being closed properly after reading in Archivable concern --------- Co-authored-by: Robin Tuszik <mail@robin.gg>
308 lines
9.8 KiB
JavaScript
308 lines
9.8 KiB
JavaScript
import L from "leaflet";
|
|
import { createAllMapLayers } from "../maps/layers";
|
|
import BaseController from "./base_controller";
|
|
|
|
export default class extends BaseController {
|
|
static targets = ["container"];
|
|
static values = {
|
|
year: Number,
|
|
month: Number,
|
|
uuid: String,
|
|
dataBounds: Object,
|
|
hexagonsAvailable: Boolean,
|
|
selfHosted: String,
|
|
timezone: String
|
|
};
|
|
|
|
connect() {
|
|
super.connect();
|
|
console.log('🏁 Controller connected - loading overlay should be visible');
|
|
this.selfHosted = this.selfHostedValue || 'false';
|
|
this.currentHexagonLayer = null;
|
|
this.initializeMap();
|
|
this.loadHexagons();
|
|
}
|
|
|
|
disconnect() {
|
|
if (this.map) {
|
|
this.map.remove();
|
|
}
|
|
}
|
|
|
|
initializeMap() {
|
|
// Initialize map with interactive controls enabled
|
|
this.map = L.map(this.element, {
|
|
zoomControl: true,
|
|
scrollWheelZoom: true,
|
|
doubleClickZoom: true,
|
|
touchZoom: true,
|
|
dragging: true,
|
|
keyboard: false
|
|
});
|
|
|
|
// Add dynamic tile layer based on self-hosted setting
|
|
this.addMapLayers();
|
|
|
|
// Default view with higher zoom level for better hexagon detail
|
|
this.map.setView([40.0, -100.0], 9);
|
|
}
|
|
|
|
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://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© OpenStreetMap contributors',
|
|
maxZoom: 15
|
|
}).addTo(this.map);
|
|
}
|
|
|
|
async loadHexagons() {
|
|
const initialLoadingElement = document.getElementById('map-loading');
|
|
|
|
try {
|
|
// Use server-provided data bounds
|
|
const dataBounds = this.dataBoundsValue;
|
|
|
|
if (dataBounds && dataBounds.point_count > 0) {
|
|
// Set map view to data bounds BEFORE creating hexagon grid
|
|
this.map.fitBounds([
|
|
[dataBounds.min_lat, dataBounds.min_lng],
|
|
[dataBounds.max_lat, dataBounds.max_lng]
|
|
], { padding: [20, 20] });
|
|
|
|
// Wait for the map to finish fitting bounds
|
|
console.log('⏳ About to wait for map moveend - overlay should still be visible');
|
|
await new Promise(resolve => {
|
|
this.map.once('moveend', resolve);
|
|
// Fallback timeout in case moveend doesn't fire
|
|
setTimeout(resolve, 1000);
|
|
});
|
|
}
|
|
|
|
// Load hexagons only if they are pre-calculated and data exists
|
|
if (dataBounds && dataBounds.point_count > 0 && this.hexagonsAvailableValue) {
|
|
await this.loadStaticHexagons();
|
|
} else {
|
|
if (!this.hexagonsAvailableValue) {
|
|
console.log('📋 No pre-calculated hexagons available for public sharing - skipping hexagon loading');
|
|
} else {
|
|
console.warn('⚠️ No data bounds or points available - not showing hexagons');
|
|
}
|
|
// Hide loading indicator if no hexagons to load
|
|
const loadingElement = document.getElementById('map-loading');
|
|
if (loadingElement) {
|
|
loadingElement.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error initializing hexagon grid:', error);
|
|
|
|
// Hide loading indicator on initialization error
|
|
const loadingElement = document.getElementById('map-loading');
|
|
if (loadingElement) {
|
|
loadingElement.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Do NOT hide loading overlay here - let loadStaticHexagons() handle it completely
|
|
}
|
|
|
|
async loadStaticHexagons() {
|
|
console.log('🔄 Loading static hexagons for public sharing...');
|
|
|
|
// Ensure loading overlay is visible and disable map interaction
|
|
const loadingElement = document.getElementById('map-loading');
|
|
|
|
if (loadingElement) {
|
|
loadingElement.style.display = 'flex';
|
|
loadingElement.style.visibility = 'visible';
|
|
loadingElement.style.zIndex = '9999';
|
|
}
|
|
|
|
// Disable map interaction during loading
|
|
this.map.dragging.disable();
|
|
this.map.touchZoom.disable();
|
|
this.map.doubleClickZoom.disable();
|
|
this.map.scrollWheelZoom.disable();
|
|
this.map.boxZoom.disable();
|
|
this.map.keyboard.disable();
|
|
if (this.map.tap) this.map.tap.disable();
|
|
|
|
// Add delay to ensure loading overlay is visible
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
try {
|
|
// Calculate date range for the month
|
|
const startDate = new Date(this.yearValue, this.monthValue - 1, 1);
|
|
const endDate = new Date(this.yearValue, this.monthValue, 0, 23, 59, 59);
|
|
|
|
// Use the full data bounds for hexagon request (not current map viewport)
|
|
const dataBounds = this.dataBoundsValue;
|
|
|
|
const params = new URLSearchParams({
|
|
min_lon: dataBounds.min_lng,
|
|
min_lat: dataBounds.min_lat,
|
|
max_lon: dataBounds.max_lng,
|
|
max_lat: dataBounds.max_lat,
|
|
start_date: startDate.toISOString(),
|
|
end_date: endDate.toISOString(),
|
|
uuid: this.uuidValue
|
|
});
|
|
|
|
const url = `/api/v1/maps/hexagons?${params}`;
|
|
console.log('📍 Fetching static hexagons from:', url);
|
|
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error('Hexagon API error:', response.status, response.statusText, errorText);
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const geojsonData = await response.json();
|
|
|
|
// Add hexagons directly to map as a static layer
|
|
if (geojsonData.features && geojsonData.features.length > 0) {
|
|
this.addStaticHexagonsToMap(geojsonData);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load static hexagons:', error);
|
|
} finally {
|
|
// Re-enable map interaction after loading (success or failure)
|
|
this.map.dragging.enable();
|
|
this.map.touchZoom.enable();
|
|
this.map.doubleClickZoom.enable();
|
|
this.map.scrollWheelZoom.enable();
|
|
this.map.boxZoom.enable();
|
|
this.map.keyboard.enable();
|
|
if (this.map.tap) this.map.tap.enable();
|
|
|
|
// Hide loading overlay
|
|
const loadingElement = document.getElementById('map-loading');
|
|
if (loadingElement) {
|
|
loadingElement.style.display = 'none';
|
|
}
|
|
}
|
|
}
|
|
|
|
addStaticHexagonsToMap(geojsonData) {
|
|
// Remove existing hexagon layer if it exists
|
|
if (this.currentHexagonLayer) {
|
|
this.map.removeLayer(this.currentHexagonLayer);
|
|
}
|
|
|
|
// Calculate max point count for color scaling
|
|
const maxPoints = Math.max(...geojsonData.features.map(f => f.properties.point_count));
|
|
|
|
const staticHexagonLayer = L.geoJSON(geojsonData, {
|
|
style: (feature) => this.styleHexagon(),
|
|
onEachFeature: (feature, layer) => {
|
|
// Add popup with statistics
|
|
const props = feature.properties;
|
|
const popupContent = this.buildPopupContent(props);
|
|
layer.bindPopup(popupContent);
|
|
|
|
// Add hover effects
|
|
layer.on({
|
|
mouseover: (e) => this.onHexagonMouseOver(e),
|
|
mouseout: (e) => this.onHexagonMouseOut(e)
|
|
});
|
|
}
|
|
});
|
|
|
|
this.currentHexagonLayer = staticHexagonLayer;
|
|
staticHexagonLayer.addTo(this.map);
|
|
}
|
|
|
|
styleHexagon() {
|
|
return {
|
|
fillColor: '#3388ff',
|
|
fillOpacity: 0.3,
|
|
color: '#3388ff',
|
|
weight: 1,
|
|
opacity: 0.3
|
|
};
|
|
}
|
|
|
|
buildPopupContent(props) {
|
|
const timezone = this.timezoneValue || 'UTC';
|
|
const startDate = props.earliest_point ? new Date(props.earliest_point).toLocaleDateString('en-US', { timeZone: timezone }) : 'N/A';
|
|
const endDate = props.latest_point ? new Date(props.latest_point).toLocaleDateString('en-US', { timeZone: timezone }) : 'N/A';
|
|
const startTime = props.earliest_point ? new Date(props.earliest_point).toLocaleTimeString('en-US', { timeZone: timezone }) : '';
|
|
const endTime = props.latest_point ? new Date(props.latest_point).toLocaleTimeString('en-US', { timeZone: timezone }) : '';
|
|
|
|
return `
|
|
<div style="font-size: 12px; line-height: 1.6; max-width: 300px;">
|
|
<strong style="color: #3388ff;">📍 Location Data</strong><br>
|
|
<div style="margin: 4px 0;">
|
|
<strong>Points:</strong> ${props.point_count || 0}
|
|
</div>
|
|
${props.h3_index ? `
|
|
<div style="margin: 4px 0;">
|
|
<strong>H3 Index:</strong><br>
|
|
<code style="font-size: 10px; background: #f5f5f5; padding: 2px;">${props.h3_index}</code>
|
|
</div>
|
|
` : ''}
|
|
<div style="margin: 4px 0;">
|
|
<strong>Time Range:</strong><br>
|
|
<small>${startDate} ${startTime}<br>→ ${endDate} ${endTime}</small>
|
|
</div>
|
|
${props.center ? `
|
|
<div style="margin: 4px 0;">
|
|
<strong>Center:</strong><br>
|
|
<small>${props.center[0].toFixed(6)}, ${props.center[1].toFixed(6)}</small>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
onHexagonMouseOver(e) {
|
|
const layer = e.target;
|
|
// Store original style before changing
|
|
if (!layer._originalStyle) {
|
|
layer._originalStyle = {
|
|
fillOpacity: layer.options.fillOpacity,
|
|
weight: layer.options.weight,
|
|
opacity: layer.options.opacity
|
|
};
|
|
}
|
|
|
|
layer.setStyle({
|
|
fillOpacity: 0.8,
|
|
weight: 2,
|
|
opacity: 1.0
|
|
});
|
|
}
|
|
|
|
onHexagonMouseOut(e) {
|
|
const layer = e.target;
|
|
// Reset to stored original style
|
|
if (layer._originalStyle) {
|
|
layer.setStyle(layer._originalStyle);
|
|
}
|
|
}
|
|
}
|