Fix maps layers

This commit is contained in:
Eugene Burmakin
2025-07-30 19:00:00 +02:00
parent 379b1a6377
commit 84c35ea5fa
7 changed files with 440 additions and 274 deletions

View File

@@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
# [0.30.7] - 2025-07-30
## Fixed
- Photos layer is now working again on the map page. #1563 #1421 #1071 #889
- Suggested and Confirmed visits layers are now working again on the map page. #1443
## Added
- Logging for Photos layer is now enabled.
# [0.30.6] - 2025-07-27
## Changed

File diff suppressed because one or more lines are too long

View File

@@ -30,7 +30,8 @@ import {
import { fetchAndDrawAreas, handleAreaCreated } from "../maps/areas";
import { showFlashMessage, fetchAndDisplayPhotos } from "../maps/helpers";
import { showFlashMessage } from "../maps/helpers";
import { fetchAndDisplayPhotos } from "../maps/photos";
import { countryCodesMap } from "../maps/country_codes";
import { VisitsManager } from "../maps/visits";
@@ -382,6 +383,8 @@ export default class extends BaseController {
}
const worldData = await response.json();
// Cache the world borders data for future use
this.worldBordersData = worldData;
const visitedCountries = this.getVisitedCountries(countryCodesMap)
const filteredFeatures = worldData.features.filter(feature =>
@@ -419,6 +422,62 @@ export default class extends BaseController {
}
}
async refreshScratchLayer() {
console.log('Refreshing scratch layer with current data');
if (!this.scratchLayer) {
console.log('Scratch layer not initialized, setting up');
await this.setupScratchLayer(this.countryCodesMap);
return;
}
try {
// Clear existing data
this.scratchLayer.clearLayers();
// Get current visited countries based on current markers
const visitedCountries = this.getVisitedCountries(this.countryCodesMap);
console.log('Current visited countries:', visitedCountries);
if (visitedCountries.length === 0) {
console.log('No visited countries found');
return;
}
// Fetch country borders data (reuse if already loaded)
if (!this.worldBordersData) {
console.log('Loading world borders data');
const response = await fetch('/api/v1/countries/borders.json', {
headers: {
'Accept': 'application/geo+json,application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
this.worldBordersData = await response.json();
}
// Filter for visited countries
const filteredFeatures = this.worldBordersData.features.filter(feature =>
visitedCountries.includes(feature.properties["ISO3166-1-Alpha-2"])
);
console.log('Filtered features for visited countries:', filteredFeatures.length);
// Add the filtered country data to the scratch layer
this.scratchLayer.addData({
type: 'FeatureCollection',
features: filteredFeatures
});
} catch (error) {
console.error('Error refreshing scratch layer:', error);
}
}
baseMaps() {
let selectedLayerName = this.userSettings.preferred_map_layer || "OpenStreetMap";
let maps = createAllMapLayers(this.map, selectedLayerName, this.selfHosted);
@@ -514,6 +573,39 @@ export default class extends BaseController {
if (this.drawControl && !this.map.hasControl && !this.map._controlCorners.topleft.querySelector('.leaflet-draw')) {
this.map.addControl(this.drawControl);
}
} else if (event.name === 'Photos') {
// Load photos when Photos layer is enabled
console.log('Photos layer enabled via layer control');
const urlParams = new URLSearchParams(window.location.search);
const startDate = urlParams.get('start_at') || new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
const endDate = urlParams.get('end_at') || new Date().toISOString();
console.log('Fetching photos for date range:', { startDate, endDate });
fetchAndDisplayPhotos({
map: this.map,
photoMarkers: this.photoMarkers,
apiKey: this.apiKey,
startDate: startDate,
endDate: endDate,
userSettings: this.userSettings
});
} else if (event.name === 'Suggested Visits' || event.name === 'Confirmed Visits') {
// Load visits when layer is enabled
console.log(`${event.name} layer enabled via layer control`);
if (this.visitsManager && typeof this.visitsManager.fetchAndDisplayVisits === 'function') {
// Fetch and populate the visits - this will create circles and update drawer if open
this.visitsManager.fetchAndDisplayVisits();
}
} else if (event.name === 'Scratch map') {
// Refresh scratch map with current visited countries
console.log('Scratch map layer enabled via layer control');
this.refreshScratchLayer();
} else if (event.name === 'Fog of War') {
// Enable fog of war when layer is added
this.fogOverlay = event.layer;
if (this.markers && this.markers.length > 0) {
this.updateFog(this.markers, this.clearFogRadius, this.fogLinethreshold);
}
}
// Manage pane visibility when layers are manually toggled
@@ -533,6 +625,13 @@ export default class extends BaseController {
if (this.drawControl && this.map._controlCorners.topleft.querySelector('.leaflet-draw')) {
this.map.removeControl(this.drawControl);
}
} else if (event.name === 'Suggested Visits') {
// Clear suggested visits when layer is disabled
console.log('Suggested Visits layer disabled via layer control');
if (this.visitsManager) {
// Clear the visit circles when layer is disabled
this.visitsManager.visitCircles.clearLayers();
}
}
});
}
@@ -1054,53 +1153,6 @@ export default class extends BaseController {
}
}
createPhotoMarker(photo) {
if (!photo.exifInfo?.latitude || !photo.exifInfo?.longitude) return;
const thumbnailUrl = `/api/v1/photos/${photo.id}/thumbnail.jpg?api_key=${this.apiKey}&source=${photo.source}`;
const icon = L.divIcon({
className: 'photo-marker',
html: `<img src="${thumbnailUrl}" style="width: 48px; height: 48px;">`,
iconSize: [48, 48]
});
const marker = L.marker(
[photo.exifInfo.latitude, photo.exifInfo.longitude],
{ icon }
);
const startOfDay = new Date(photo.localDateTime);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(photo.localDateTime);
endOfDay.setHours(23, 59, 59, 999);
const queryParams = {
takenAfter: startOfDay.toISOString(),
takenBefore: endOfDay.toISOString()
};
const encodedQuery = encodeURIComponent(JSON.stringify(queryParams));
const immich_photo_link = `${this.userSettings.immich_url}/search?query=${encodedQuery}`;
const popupContent = `
<div class="max-w-xs">
<a href="${immich_photo_link}" target="_blank" onmouseover="this.firstElementChild.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.3)';"
onmouseout="this.firstElementChild.style.boxShadow = '';">
<img src="${thumbnailUrl}"
class="w-8 h-8 mb-2 rounded"
style="transition: box-shadow 0.3s ease;"
alt="${photo.originalFileName}">
</a>
<h3 class="font-bold">${photo.originalFileName}</h3>
<p>Taken: ${new Date(photo.localDateTime).toLocaleString()}</p>
<p>Location: ${photo.exifInfo.city}, ${photo.exifInfo.state}, ${photo.exifInfo.country}</p>
${photo.type === 'video' ? '🎥 Video' : '📷 Photo'}
</div>
`;
marker.bindPopup(popupContent, { autoClose: false });
this.photoMarkers.addLayer(marker);
}
addTogglePanelButton() {
const TogglePanelControl = L.Control.extend({
@@ -1305,7 +1357,20 @@ export default class extends BaseController {
// Initialize photos layer if user wants it visible
if (this.userSettings.photos_enabled) {
fetchAndDisplayPhotos(this.photoMarkers, this.apiKey, this.userSettings);
console.log('Photos layer enabled via user settings');
const urlParams = new URLSearchParams(window.location.search);
const startDate = urlParams.get('start_at') || new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
const endDate = urlParams.get('end_at') || new Date().toISOString();
console.log('Auto-fetching photos for date range:', { startDate, endDate });
fetchAndDisplayPhotos({
map: this.map,
photoMarkers: this.photoMarkers,
apiKey: this.apiKey,
startDate: startDate,
endDate: endDate,
userSettings: this.userSettings
});
}
// Initialize fog of war if enabled in settings
@@ -1314,8 +1379,17 @@ export default class extends BaseController {
}
// Initialize visits manager functionality
// Check if any visits layers are enabled by default and load data
if (this.visitsManager && typeof this.visitsManager.fetchAndDisplayVisits === 'function') {
this.visitsManager.fetchAndDisplayVisits();
// Check if confirmed visits layer is enabled by default (it's added to map in constructor)
const confirmedVisitsEnabled = this.map.hasLayer(this.visitsManager.getConfirmedVisitCirclesLayer());
console.log('Visits initialization - confirmedVisitsEnabled:', confirmedVisitsEnabled);
if (confirmedVisitsEnabled) {
console.log('Confirmed visits layer enabled by default - fetching visits data');
this.visitsManager.fetchAndDisplayVisits();
}
}
}

View File

@@ -7,10 +7,8 @@ import BaseController from "./base_controller"
import L from "leaflet"
import { createAllMapLayers } from "../maps/layers"
import { createPopupContent } from "../maps/popups"
import {
fetchAndDisplayPhotos,
showFlashMessage
} from '../maps/helpers';
import { showFlashMessage } from '../maps/helpers';
import { fetchAndDisplayPhotos } from '../maps/photos';
export default class extends BaseController {
static targets = ["container", "startedAt", "endedAt"]

View File

@@ -189,159 +189,6 @@ function classesForFlash(type) {
}
}
export async function fetchAndDisplayPhotos({ map, photoMarkers, apiKey, startDate, endDate, userSettings }, retryCount = 0) {
const MAX_RETRIES = 3;
const RETRY_DELAY = 3000; // 3 seconds
// Create loading control
const LoadingControl = L.Control.extend({
onAdd: (map) => {
const container = L.DomUtil.create('div', 'leaflet-loading-control');
container.innerHTML = '<div class="loading-spinner"></div>';
return container;
}
});
const loadingControl = new LoadingControl({ position: 'topleft' });
map.addControl(loadingControl);
try {
const params = new URLSearchParams({
api_key: apiKey,
start_date: startDate,
end_date: endDate
});
const response = await fetch(`/api/v1/photos?${params}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}, response: ${response.body}`);
}
const photos = await response.json();
photoMarkers.clearLayers();
const photoLoadPromises = photos.map(photo => {
return new Promise((resolve) => {
const img = new Image();
const thumbnailUrl = `/api/v1/photos/${photo.id}/thumbnail.jpg?api_key=${apiKey}&source=${photo.source}`;
img.onload = () => {
createPhotoMarker(photo, userSettings, photoMarkers, apiKey);
resolve();
};
img.onerror = () => {
console.error(`Failed to load photo ${photo.id}`);
resolve(); // Resolve anyway to not block other photos
};
img.src = thumbnailUrl;
});
});
await Promise.all(photoLoadPromises);
if (!map.hasLayer(photoMarkers)) {
photoMarkers.addTo(map);
}
// Show checkmark for 1 second before removing
const loadingSpinner = document.querySelector('.loading-spinner');
loadingSpinner.classList.add('done');
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (error) {
console.error('Error fetching photos:', error);
showFlashMessage('error', 'Failed to fetch photos');
if (retryCount < MAX_RETRIES) {
console.log(`Retrying in ${RETRY_DELAY/1000} seconds... (Attempt ${retryCount + 1}/${MAX_RETRIES})`);
setTimeout(() => {
fetchAndDisplayPhotos({ map, photoMarkers, apiKey, startDate, endDate }, retryCount + 1);
}, RETRY_DELAY);
} else {
showFlashMessage('error', 'Failed to fetch photos after multiple attempts');
}
} finally {
map.removeControl(loadingControl);
}
}
function getPhotoLink(photo, userSettings) {
switch (photo.source) {
case 'immich':
const startOfDay = new Date(photo.localDateTime);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(photo.localDateTime);
endOfDay.setHours(23, 59, 59, 999);
const queryParams = {
takenAfter: startOfDay.toISOString(),
takenBefore: endOfDay.toISOString()
};
const encodedQuery = encodeURIComponent(JSON.stringify(queryParams));
return `${userSettings.immich_url}/search?query=${encodedQuery}`;
case 'photoprism':
return `${userSettings.photoprism_url}/library/browse?view=cards&year=${photo.localDateTime.split('-')[0]}&month=${photo.localDateTime.split('-')[1]}&order=newest&public=true&quality=3`;
default:
return '#'; // Default or error case
}
}
function getSourceUrl(photo, userSettings) {
switch (photo.source) {
case 'photoprism':
return userSettings.photoprism_url;
case 'immich':
return userSettings.immich_url;
default:
return '#'; // Default or error case
}
}
export function createPhotoMarker(photo, userSettings, photoMarkers, apiKey) {
if (!photo.latitude || !photo.longitude) return;
const thumbnailUrl = `/api/v1/photos/${photo.id}/thumbnail.jpg?api_key=${apiKey}&source=${photo.source}`;
const icon = L.divIcon({
className: 'photo-marker',
html: `<img src="${thumbnailUrl}" style="width: 48px; height: 48px;">`,
iconSize: [48, 48]
});
const marker = L.marker(
[photo.latitude, photo.longitude],
{ icon }
);
const photo_link = getPhotoLink(photo, userSettings);
const source_url = getSourceUrl(photo, userSettings);
const popupContent = `
<div class="max-w-xs">
<a href="${photo_link}" target="_blank" onmouseover="this.firstElementChild.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.3)';"
onmouseout="this.firstElementChild.style.boxShadow = '';">
<img src="${thumbnailUrl}"
class="mb-2 rounded"
style="transition: box-shadow 0.3s ease;"
alt="${photo.originalFileName}">
</a>
<h3 class="font-bold">${photo.originalFileName}</h3>
<p>Taken: ${new Date(photo.localDateTime).toLocaleString()}</p>
<p>Location: ${photo.city}, ${photo.state}, ${photo.country}</p>
<p>Source: <a href="${source_url}" target="_blank">${photo.source}</a></p>
${photo.type === 'VIDEO' ? '🎥 Video' : '📷 Photo'}
</div>
`;
marker.bindPopup(popupContent);
photoMarkers.addLayer(marker);
}
export function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
@@ -352,4 +199,4 @@ export function debounce(func, wait) {
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
}

View File

@@ -0,0 +1,190 @@
// javascript/maps/photos.js
import L from "leaflet";
import { showFlashMessage } from "./helpers";
export async function fetchAndDisplayPhotos({ map, photoMarkers, apiKey, startDate, endDate, userSettings }, retryCount = 0) {
const MAX_RETRIES = 3;
const RETRY_DELAY = 3000; // 3 seconds
console.log('fetchAndDisplayPhotos called with:', {
startDate,
endDate,
retryCount,
photoMarkersExists: !!photoMarkers,
mapExists: !!map,
apiKeyExists: !!apiKey,
userSettingsExists: !!userSettings
});
// Create loading control
const LoadingControl = L.Control.extend({
onAdd: (map) => {
const container = L.DomUtil.create('div', 'leaflet-loading-control');
container.innerHTML = '<div class="loading-spinner"></div>';
return container;
}
});
const loadingControl = new LoadingControl({ position: 'topleft' });
map.addControl(loadingControl);
try {
const params = new URLSearchParams({
api_key: apiKey,
start_date: startDate,
end_date: endDate
});
console.log('Fetching photos from API:', `/api/v1/photos?${params}`);
const response = await fetch(`/api/v1/photos?${params}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}, response: ${response.body}`);
}
const photos = await response.json();
console.log('Photos API response:', { count: photos.length, photos });
photoMarkers.clearLayers();
const photoLoadPromises = photos.map(photo => {
return new Promise((resolve) => {
const img = new Image();
const thumbnailUrl = `/api/v1/photos/${photo.id}/thumbnail.jpg?api_key=${apiKey}&source=${photo.source}`;
img.onload = () => {
console.log('Photo thumbnail loaded, creating marker for:', photo.id);
createPhotoMarker(photo, userSettings, photoMarkers, apiKey);
resolve();
};
img.onerror = () => {
console.error(`Failed to load photo ${photo.id}`);
resolve(); // Resolve anyway to not block other photos
};
img.src = thumbnailUrl;
});
});
await Promise.all(photoLoadPromises);
console.log('All photo markers created, adding to map');
if (!map.hasLayer(photoMarkers)) {
photoMarkers.addTo(map);
console.log('Photos layer added to map');
} else {
console.log('Photos layer already on map');
}
// Show checkmark for 1 second before removing
const loadingSpinner = document.querySelector('.loading-spinner');
loadingSpinner.classList.add('done');
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Photos loading completed successfully');
} catch (error) {
console.error('Error fetching photos:', error);
showFlashMessage('error', 'Failed to fetch photos');
if (retryCount < MAX_RETRIES) {
console.log(`Retrying in ${RETRY_DELAY/1000} seconds... (Attempt ${retryCount + 1}/${MAX_RETRIES})`);
setTimeout(() => {
fetchAndDisplayPhotos({ map, photoMarkers, apiKey, startDate, endDate, userSettings }, retryCount + 1);
}, RETRY_DELAY);
} else {
showFlashMessage('error', 'Failed to fetch photos after multiple attempts');
}
} finally {
map.removeControl(loadingControl);
}
}
function getPhotoLink(photo, userSettings) {
switch (photo.source) {
case 'immich':
const startOfDay = new Date(photo.localDateTime);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(photo.localDateTime);
endOfDay.setHours(23, 59, 59, 999);
const queryParams = {
takenAfter: startOfDay.toISOString(),
takenBefore: endOfDay.toISOString()
};
const encodedQuery = encodeURIComponent(JSON.stringify(queryParams));
return `${userSettings.immich_url}/search?query=${encodedQuery}`;
case 'photoprism':
return `${userSettings.photoprism_url}/library/browse?view=cards&year=${photo.localDateTime.split('-')[0]}&month=${photo.localDateTime.split('-')[1]}&order=newest&public=true&quality=3`;
default:
return '#'; // Default or error case
}
}
function getSourceUrl(photo, userSettings) {
switch (photo.source) {
case 'photoprism':
return userSettings.photoprism_url;
case 'immich':
return userSettings.immich_url;
default:
return '#'; // Default or error case
}
}
export function createPhotoMarker(photo, userSettings, photoMarkers, apiKey) {
// Handle both data formats - check for exifInfo or direct lat/lng
const latitude = photo.latitude || photo.exifInfo?.latitude;
const longitude = photo.longitude || photo.exifInfo?.longitude;
console.log('Creating photo marker for:', {
photoId: photo.id,
latitude,
longitude,
hasExifInfo: !!photo.exifInfo,
hasDirectCoords: !!(photo.latitude && photo.longitude)
});
if (!latitude || !longitude) {
console.warn('Photo missing coordinates, skipping:', photo.id);
return;
}
const thumbnailUrl = `/api/v1/photos/${photo.id}/thumbnail.jpg?api_key=${apiKey}&source=${photo.source}`;
const icon = L.divIcon({
className: 'photo-marker',
html: `<img src="${thumbnailUrl}" style="width: 48px; height: 48px;">`,
iconSize: [48, 48]
});
const marker = L.marker(
[latitude, longitude],
{ icon }
);
const photo_link = getPhotoLink(photo, userSettings);
const source_url = getSourceUrl(photo, userSettings);
const popupContent = `
<div class="max-w-xs">
<a href="${photo_link}" target="_blank" onmouseover="this.firstElementChild.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.3)';"
onmouseout="this.firstElementChild.style.boxShadow = '';">
<img src="${thumbnailUrl}"
class="mb-2 rounded"
style="transition: box-shadow 0.3s ease;"
alt="${photo.originalFileName}">
</a>
<h3 class="font-bold">${photo.originalFileName}</h3>
<p>Taken: ${new Date(photo.localDateTime).toLocaleString()}</p>
<p>Location: ${photo.city}, ${photo.state}, ${photo.country}</p>
<p>Source: <a href="${source_url}" target="_blank">${photo.source}</a></p>
${photo.type === 'VIDEO' ? '🎥 Video' : '📷 Photo'}
</div>
`;
marker.bindPopup(popupContent);
photoMarkers.addLayer(marker);
console.log('Photo marker added to layer group');
}

View File

@@ -233,15 +233,9 @@ export class VisitsManager {
this.visitCircles.clearLayers();
this.confirmedVisitCircles.clearLayers();
// If the drawer is open, refresh with time-based visits
if (this.drawerOpen) {
this.fetchAndDisplayVisits();
} else {
// If drawer is closed, we should hide all visits
if (this.map.hasLayer(this.visitCircles)) {
this.map.removeLayer(this.visitCircles);
}
}
// Always refresh visits data regardless of drawer state
// Layer visibility is now controlled by the layer control, not the drawer
this.fetchAndDisplayVisits();
// Reset drawer title
const drawerTitle = document.querySelector('#visits-drawer .drawer h2');
@@ -495,19 +489,16 @@ export class VisitsManager {
control.classList.toggle('controls-shifted');
});
// Update the drawer content if it's being opened
// Update the drawer content if it's being opened - but don't fetch visits automatically
if (this.drawerOpen) {
this.fetchAndDisplayVisits();
// Show the suggested visits layer when drawer is open
if (!this.map.hasLayer(this.visitCircles)) {
this.map.addLayer(this.visitCircles);
}
} else {
// Hide the suggested visits layer when drawer is closed
if (this.map.hasLayer(this.visitCircles)) {
this.map.removeLayer(this.visitCircles);
console.log('Drawer opened - showing placeholder message');
// Just show a placeholder message in the drawer, don't fetch visits
const container = document.getElementById('visits-list');
if (container) {
container.innerHTML = '<p class="text-gray-500">Enable "Suggested Visits" or "Confirmed Visits" layers to see visits data</p>';
}
}
// Note: Layer visibility is now controlled by the layer control, not the drawer state
}
/**
@@ -546,11 +537,13 @@ export class VisitsManager {
*/
async fetchAndDisplayVisits() {
try {
console.log('fetchAndDisplayVisits called');
// Clear any existing highlight before fetching new visits
this.clearVisitHighlight();
// If there's an active selection, don't perform time-based fetch
if (this.isSelectionActive && this.selectionRect) {
console.log('Active selection found, fetching visits in selection');
this.fetchVisitsInSelection();
return;
}
@@ -560,7 +553,7 @@ export class VisitsManager {
const startAt = urlParams.get('start_at') || new Date().toISOString();
const endAt = urlParams.get('end_at') || new Date().toISOString();
console.log('Fetching visits for:', startAt, endAt);
console.log('Fetching visits for date range:', { startAt, endAt });
const response = await fetch(
`/api/v1/visits?start_at=${encodeURIComponent(startAt)}&end_at=${encodeURIComponent(endAt)}`,
{
@@ -573,22 +566,35 @@ export class VisitsManager {
);
if (!response.ok) {
console.error('Visits API response not ok:', response.status, response.statusText);
throw new Error('Network response was not ok');
}
const visits = await response.json();
console.log('Visits API response:', { count: visits.length, visits });
this.displayVisits(visits);
// Ensure the suggested visits layer visibility matches the drawer state
if (this.drawerOpen) {
if (!this.map.hasLayer(this.visitCircles)) {
this.map.addLayer(this.visitCircles);
// Let the layer control manage visibility instead of drawer state
console.log('Visit circles populated - layer control will manage visibility');
console.log('visitCircles layer count:', this.visitCircles.getLayers().length);
console.log('confirmedVisitCircles layer count:', this.confirmedVisitCircles.getLayers().length);
// Check if the layers are currently enabled in the layer control and ensure they're visible
const layerControl = this.map._layers;
let suggestedVisitsEnabled = false;
let confirmedVisitsEnabled = false;
// Check layer control state
Object.values(layerControl || {}).forEach(layer => {
if (layer.name === 'Suggested Visits' && this.map.hasLayer(layer.layer)) {
suggestedVisitsEnabled = true;
}
} else {
if (this.map.hasLayer(this.visitCircles)) {
this.map.removeLayer(this.visitCircles);
if (layer.name === 'Confirmed Visits' && this.map.hasLayer(layer.layer)) {
confirmedVisitsEnabled = true;
}
}
});
console.log('Layer control state:', { suggestedVisitsEnabled, confirmedVisitsEnabled });
} catch (error) {
console.error('Error fetching visits:', error);
const container = document.getElementById('visits-list');
@@ -598,13 +604,88 @@ export class VisitsManager {
}
}
/**
* Creates visit circles on the map (independent of drawer UI)
* @param {Array} visits - Array of visit objects
*/
createMapCircles(visits) {
if (!visits || visits.length === 0) {
console.log('No visits to create circles for');
return;
}
// Clear existing visit circles
console.log('Clearing existing visit circles');
this.visitCircles.clearLayers();
this.confirmedVisitCircles.clearLayers();
let suggestedCount = 0;
let confirmedCount = 0;
// Draw circles for all visits
visits
.filter(visit => visit.status !== 'declined')
.forEach(visit => {
if (visit.place?.latitude && visit.place?.longitude) {
const isConfirmed = visit.status === 'confirmed';
const isSuggested = visit.status === 'suggested';
console.log('Creating circle for visit:', {
id: visit.id,
status: visit.status,
lat: visit.place.latitude,
lng: visit.place.longitude,
isConfirmed,
isSuggested
});
const circle = L.circle([visit.place.latitude, visit.place.longitude], {
color: isSuggested ? '#FFA500' : '#4A90E2', // Border color
fillColor: isSuggested ? '#FFD700' : '#4A90E2', // Fill color
fillOpacity: isSuggested ? 0.3 : 0.5,
radius: isConfirmed ? 110 : 80, // Increased size for confirmed visits
weight: 2,
interactive: true,
bubblingMouseEvents: false,
pane: isConfirmed ? 'confirmedVisitsPane' : 'suggestedVisitsPane', // Use appropriate pane
dashArray: isSuggested ? '4' : null // Dotted border for suggested
});
// Add the circle to the appropriate layer
if (isConfirmed) {
this.confirmedVisitCircles.addLayer(circle);
confirmedCount++;
console.log('Added confirmed visit circle to layer');
} else {
this.visitCircles.addLayer(circle);
suggestedCount++;
console.log('Added suggested visit circle to layer');
}
// Attach click event to the circle
circle.on('click', () => this.fetchPossiblePlaces(visit));
} else {
console.warn('Visit missing coordinates:', visit);
}
});
console.log('Visit circles created:', { suggestedCount, confirmedCount });
}
/**
* Displays visits on the map and in the drawer
* @param {Array} visits - Array of visit objects
*/
displayVisits(visits) {
// Always create map circles regardless of drawer state
this.createMapCircles(visits);
// Update drawer UI only if container exists
const container = document.getElementById('visits-list');
if (!container) return;
if (!container) {
console.log('No visits-list container found - skipping drawer UI update');
return;
}
// Update the drawer title if selection is active
if (this.isSelectionActive && this.selectionRect) {
@@ -637,42 +718,7 @@ export class VisitsManager {
return;
}
// Clear existing visit circles
this.visitCircles.clearLayers();
this.confirmedVisitCircles.clearLayers();
// Draw circles for all visits
visits
.filter(visit => visit.status !== 'declined')
.forEach(visit => {
if (visit.place?.latitude && visit.place?.longitude) {
const isConfirmed = visit.status === 'confirmed';
const isSuggested = visit.status === 'suggested';
const circle = L.circle([visit.place.latitude, visit.place.longitude], {
color: isSuggested ? '#FFA500' : '#4A90E2', // Border color
fillColor: isSuggested ? '#FFD700' : '#4A90E2', // Fill color
fillOpacity: isSuggested ? 0.3 : 0.5,
radius: isConfirmed ? 110 : 80, // Increased size for confirmed visits
weight: 2,
interactive: true,
bubblingMouseEvents: false,
pane: isConfirmed ? 'confirmedVisitsPane' : 'suggestedVisitsPane', // Use appropriate pane
dashArray: isSuggested ? '4' : null // Dotted border for suggested
});
// Add the circle to the appropriate layer
if (isConfirmed) {
this.confirmedVisitCircles.addLayer(circle);
} else {
this.visitCircles.addLayer(circle);
}
// Attach click event to the circle
circle.on('click', () => this.fetchPossiblePlaces(visit));
}
});
// Map circles are handled by createMapCircles() - just generate drawer HTML
const visitsHtml = visits
// Filter out declined visits
.filter(visit => visit.status !== 'declined')