freealberta/influence/app/public/js/representatives-map.js

583 lines
22 KiB
JavaScript

/**
* Representatives Map Module
* Handles map initialization, office location display, and popup cards
*/
// Map state
let representativesMap = null;
let representativeMarkers = [];
let currentPostalCode = null;
// Office location icons
const officeIcons = {
federal: L.divIcon({
className: 'office-marker federal',
html: '<div class="marker-content">🏛️</div>',
iconSize: [40, 40],
iconAnchor: [20, 40],
popupAnchor: [0, -40]
}),
provincial: L.divIcon({
className: 'office-marker provincial',
html: '<div class="marker-content">🏢</div>',
iconSize: [40, 40],
iconAnchor: [20, 40],
popupAnchor: [0, -40]
}),
municipal: L.divIcon({
className: 'office-marker municipal',
html: '<div class="marker-content">🏛️</div>',
iconSize: [40, 40],
iconAnchor: [20, 40],
popupAnchor: [0, -40]
})
};
// Initialize the representatives map
function initializeRepresentativesMap() {
const mapContainer = document.getElementById('main-map');
if (!mapContainer) {
console.warn('Map container not found');
return;
}
// Avoid double initialization
if (representativesMap) {
console.log('Map already initialized, invalidating size instead');
representativesMap.invalidateSize();
return;
}
// Check if Leaflet is available
if (typeof L === 'undefined') {
console.error('Leaflet (L) is not defined. Map initialization failed.');
return;
}
// We'll initialize the map even if not visible, then invalidate size when needed
console.log('Initializing representatives map...');
// Center on Alberta
representativesMap = L.map('main-map').setView([53.9333, -116.5765], 6);
// Add tile layer
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19,
minZoom: 2
}).addTo(representativesMap);
// Trigger size invalidation after a brief moment to ensure proper rendering
setTimeout(() => {
if (representativesMap) {
representativesMap.invalidateSize();
}
}, 200);
}
// Clear all representative markers from the map
function clearRepresentativeMarkers() {
representativeMarkers.forEach(marker => {
representativesMap.removeLayer(marker);
});
representativeMarkers = [];
}
// Add representative offices to the map
function displayRepresentativeOffices(representatives, postalCode) {
// Initialize map if not already done
if (!representativesMap) {
console.log('Map not initialized, initializing now...');
initializeRepresentativesMap();
}
if (!representativesMap) {
console.error('Failed to initialize map');
return;
}
clearRepresentativeMarkers();
currentPostalCode = postalCode;
const validOffices = [];
let bounds = [];
console.log('Processing representatives for map display:', representatives.length);
// Group representatives by office location to handle shared addresses
const locationGroups = new Map();
representatives.forEach((rep, index) => {
console.log(`Processing representative ${index + 1}:`, rep.name, rep.representative_set_name);
// Try to get office location from various sources
const offices = getOfficeLocations(rep);
console.log(`Found ${offices.length} offices for ${rep.name}:`, offices);
offices.forEach((office, officeIndex) => {
console.log(`Office ${officeIndex + 1} for ${rep.name}:`, office);
if (office.lat && office.lng) {
const locationKey = `${office.lat.toFixed(6)},${office.lng.toFixed(6)}`;
if (!locationGroups.has(locationKey)) {
locationGroups.set(locationKey, {
lat: office.lat,
lng: office.lng,
address: office.address,
representatives: [],
offices: []
});
}
locationGroups.get(locationKey).representatives.push(rep);
locationGroups.get(locationKey).offices.push(office);
validOffices.push({ rep, office });
} else {
console.log(`No coordinates found for ${rep.name} office:`, office);
}
});
});
// Create markers for each location group
let offsetIndex = 0;
locationGroups.forEach((locationGroup, locationKey) => {
console.log(`Creating markers for location ${locationKey} with ${locationGroup.representatives.length} representatives`);
if (locationGroup.representatives.length === 1) {
// Single representative at this location
const rep = locationGroup.representatives[0];
const office = locationGroup.offices[0];
const marker = createOfficeMarker(rep, office);
if (marker) {
representativeMarkers.push(marker);
marker.addTo(representativesMap);
bounds.push([office.lat, office.lng]);
}
} else {
// Multiple representatives at same location - create offset markers
locationGroup.representatives.forEach((rep, repIndex) => {
const office = locationGroup.offices[repIndex];
// Add small offset to avoid exact overlap
const offsetDistance = 0.0005; // About 50 meters
const angle = (repIndex * 2 * Math.PI) / locationGroup.representatives.length;
const offsetLat = office.lat + (offsetDistance * Math.cos(angle));
const offsetLng = office.lng + (offsetDistance * Math.sin(angle));
const offsetOffice = {
...office,
lat: offsetLat,
lng: offsetLng
};
console.log(`Creating offset marker for ${rep.name} at ${offsetLat}, ${offsetLng}`);
const marker = createOfficeMarker(rep, offsetOffice, locationGroup.representatives.length > 1);
if (marker) {
representativeMarkers.push(marker);
marker.addTo(representativesMap);
bounds.push([offsetLat, offsetLng]);
}
});
}
});
console.log(`Total markers created: ${representativeMarkers.length}`);
console.log(`Bounds array:`, bounds);
// Fit map to show all offices, or center on Alberta if no offices found
if (bounds.length > 0) {
representativesMap.fitBounds(bounds, { padding: [20, 20] });
} else {
// If no office locations found, show a message and keep Alberta view
console.log('No office locations with coordinates found, showing message');
showMapMessage('Office locations not available for representatives in this area.');
}
console.log(`Displayed ${validOffices.length} office locations on map`);
}
// Extract office locations from representative data
function getOfficeLocations(representative) {
const offices = [];
console.log(`Getting office locations for ${representative.name}`);
console.log('Representative offices data:', representative.offices);
// Check various sources for office location data
if (representative.offices && Array.isArray(representative.offices)) {
representative.offices.forEach((office, index) => {
console.log(`Processing office ${index + 1}:`, office);
// Use the 'postal' field which contains the address
if (office.postal || office.address) {
const officeData = {
type: office.type || 'office',
address: office.postal || office.address || 'Office Address',
postal_code: office.postal_code,
phone: office.tel || office.phone,
fax: office.fax,
lat: office.lat,
lng: office.lng
};
console.log('Created office data:', officeData);
offices.push(officeData);
}
});
}
// For all offices without coordinates, add approximate coordinates
offices.forEach(office => {
if (!office.lat || !office.lng) {
console.log(`Adding coordinates to office for ${representative.name}`);
const approxLocation = getApproximateLocationByDistrict(representative.district_name, representative.representative_set_name);
console.log('Approximate location:', approxLocation);
if (approxLocation) {
office.lat = approxLocation.lat;
office.lng = approxLocation.lng;
console.log('Updated office with coordinates:', office);
}
}
});
// If no offices found at all, create a fallback office
if (offices.length === 0 && representative.representative_set_name) {
console.log(`No offices found, creating fallback office for ${representative.name}`);
const approxLocation = getApproximateLocationByDistrict(representative.district_name, representative.representative_set_name);
console.log('Approximate location:', approxLocation);
if (approxLocation) {
const fallbackOffice = {
type: 'representative',
address: `${representative.name} - ${representative.district_name || representative.representative_set_name}`,
lat: approxLocation.lat,
lng: approxLocation.lng
};
console.log('Created fallback office:', fallbackOffice);
offices.push(fallbackOffice);
}
}
console.log(`Total offices found for ${representative.name}:`, offices.length);
return offices;
}
// Get approximate location based on district and government level
function getApproximateLocationByDistrict(district, level) {
// Specific locations for Edmonton officials
const edmontonLocations = {
// City Hall for municipal officials
'Edmonton': { lat: 53.5444, lng: -113.4909 }, // Edmonton City Hall
"O-day'min": { lat: 53.5444, lng: -113.4909 }, // Edmonton City Hall
// Provincial Legislature
'Edmonton-Glenora': { lat: 53.5344, lng: -113.5065 }, // Alberta Legislature
// Federal offices (approximate downtown Edmonton)
'Edmonton Centre': { lat: 53.5461, lng: -113.4938 }
};
// Try specific district first
if (district && edmontonLocations[district]) {
return edmontonLocations[district];
}
// Fallback based on government level
const levelLocations = {
'House of Commons': { lat: 53.5461, lng: -113.4938 }, // Downtown Edmonton
'Legislative Assembly of Alberta': { lat: 53.5344, lng: -113.5065 }, // Alberta Legislature
'Edmonton City Council': { lat: 53.5444, lng: -113.4909 } // Edmonton City Hall
};
return levelLocations[level] || { lat: 53.9333, lng: -116.5765 }; // Default to Alberta center
}
// Create a marker for an office location
function createOfficeMarker(representative, office, isSharedLocation = false) {
if (!office.lat || !office.lng) {
return null;
}
// Determine icon based on government level
let icon = officeIcons.municipal; // default
if (representative.representative_set_name) {
if (representative.representative_set_name.includes('House of Commons')) {
icon = officeIcons.federal;
} else if (representative.representative_set_name.includes('Legislative Assembly')) {
icon = officeIcons.provincial;
}
}
const marker = L.marker([office.lat, office.lng], { icon });
// Create popup content
const popupContent = createOfficePopupContent(representative, office, isSharedLocation);
marker.bindPopup(popupContent, {
maxWidth: 300,
className: 'office-popup'
});
return marker;
}
// Create popup content for office markers
function createOfficePopupContent(representative, office, isSharedLocation = false) {
const level = getRepresentativeLevel(representative.representative_set_name);
const levelClass = level.toLowerCase().replace(' ', '-');
return `
<div class="office-popup-content">
<div class="rep-header ${levelClass}">
${representative.photo_url ? `<img src="${representative.photo_url}" alt="${representative.name}" class="rep-photo-small">` : ''}
<div class="rep-info">
<h4>${representative.name}</h4>
<p class="rep-level">${level}</p>
<p class="rep-district">${representative.district_name || 'District not specified'}</p>
${isSharedLocation ? '<p class="shared-location-note"><small><em>Note: Office location shared with other representatives</em></small></p>' : ''}
</div>
</div>
<div class="office-details">
<h5>Office Information</h5>
${office.address ? `<p><strong>Address:</strong> ${office.address}</p>` : ''}
${office.phone ? `<p><strong>Phone:</strong> <a href="tel:${office.phone}">${office.phone}</a></p>` : ''}
${office.fax ? `<p><strong>Fax:</strong> ${office.fax}</p>` : ''}
${office.postal_code ? `<p><strong>Postal Code:</strong> ${office.postal_code}</p>` : ''}
</div>
<div class="office-actions">
${representative.email ? `<button class="btn btn-primary btn-small email-btn" data-email="${representative.email}" data-name="${representative.name}" data-level="${representative.representative_set_name}">Send Email</button>` : ''}
</div>
</div>
`;
}// Get representative level for display
function getRepresentativeLevel(representativeSetName) {
if (!representativeSetName) return 'Representative';
if (representativeSetName.includes('House of Commons')) {
return 'Federal MP';
} else if (representativeSetName.includes('Legislative Assembly')) {
return 'Provincial MLA';
} else {
return 'Municipal Representative';
}
}
// Show a message on the map
function showMapMessage(message) {
const mapContainer = document.getElementById('main-map');
if (!mapContainer) return;
// Remove any existing message
const existingMessage = mapContainer.querySelector('.map-message');
if (existingMessage) {
existingMessage.remove();
}
// Create and show new message
const messageDiv = document.createElement('div');
messageDiv.className = 'map-message';
messageDiv.innerHTML = `
<div class="map-message-content">
<p>${message}</p>
</div>
`;
mapContainer.appendChild(messageDiv);
// Remove message after 5 seconds
setTimeout(() => {
if (messageDiv.parentNode) {
messageDiv.remove();
}
}, 5000);
}
// Initialize form handlers
function initializePostalForm() {
const postalForm = document.getElementById('postal-form');
// Handle postal code form submission
if (postalForm) {
postalForm.addEventListener('submit', (event) => {
event.preventDefault();
const postalCode = document.getElementById('postal-code').value.trim();
if (postalCode) {
handlePostalCodeSubmission(postalCode);
}
});
}
// Handle email button clicks in popups
document.addEventListener('click', (event) => {
if (event.target.classList.contains('email-btn')) {
event.preventDefault();
const email = event.target.dataset.email;
const name = event.target.dataset.name;
const level = event.target.dataset.level;
if (window.openEmailModal) {
window.openEmailModal(email, name, level);
}
}
});
}
// Handle postal code submission and fetch representatives
async function handlePostalCodeSubmission(postalCode) {
try {
showLoading();
hideError();
// Normalize postal code
const normalizedPostalCode = postalCode.toUpperCase().replace(/\s/g, '');
// Fetch representatives data
const response = await fetch(`/api/representatives/by-postal/${normalizedPostalCode}`);
const data = await response.json();
hideLoading();
if (data.success && data.data && data.data.representatives) {
// Display representatives on map
displayRepresentativeOffices(data.data.representatives, normalizedPostalCode);
// Also update the representatives display section using the existing system
if (window.representativesDisplay) {
window.representativesDisplay.displayRepresentatives(data.data.representatives);
}
// Update location info manually if the existing system doesn't work
const locationDetails = document.getElementById('location-details');
if (locationDetails && data.data.location) {
const location = data.data.location;
locationDetails.textContent = `${location.city}, ${location.province} (${normalizedPostalCode})`;
} else if (locationDetails) {
locationDetails.textContent = `Postal Code: ${normalizedPostalCode}`;
}
if (window.locationInfo) {
window.locationInfo.updateLocationInfo(data.data.location, normalizedPostalCode);
}
// Show the representatives section
const representativesSection = document.getElementById('representatives-section');
representativesSection.style.display = 'block';
// Fix map rendering after section becomes visible
setTimeout(() => {
if (!representativesMap) {
initializeRepresentativesMap();
}
if (representativesMap) {
representativesMap.invalidateSize();
// Try to fit bounds again if we have markers
if (representativeMarkers.length > 0) {
const bounds = representativeMarkers.map(marker => marker.getLatLng());
if (bounds.length > 0) {
representativesMap.fitBounds(bounds, { padding: [20, 20] });
}
}
}
}, 300);
// Show refresh button
const refreshBtn = document.getElementById('refresh-btn');
if (refreshBtn) {
refreshBtn.style.display = 'inline-block';
// Store postal code for refresh functionality
refreshBtn.dataset.postalCode = normalizedPostalCode;
}
// Show success message
if (window.messageDisplay) {
window.messageDisplay.show(`Found ${data.data.representatives.length} representatives for ${normalizedPostalCode}`, 'success', 3000);
}
} else {
showError(data.message || 'Unable to find representatives for this postal code.');
}
} catch (error) {
hideLoading();
console.error('Error fetching representatives:', error);
showError('An error occurred while looking up representatives. Please try again.');
}
}
// Utility functions for loading and error states
function showLoading() {
const loading = document.getElementById('loading');
if (loading) loading.style.display = 'block';
}
function hideLoading() {
const loading = document.getElementById('loading');
if (loading) loading.style.display = 'none';
}
function showError(message) {
const errorDiv = document.getElementById('error-message');
if (errorDiv) {
errorDiv.textContent = message;
errorDiv.style.display = 'block';
}
}
function hideError() {
const errorDiv = document.getElementById('error-message');
if (errorDiv) {
errorDiv.style.display = 'none';
}
}
// Initialize form handlers
function initializePostalForm() {
const postalForm = document.getElementById('postal-form');
const refreshBtn = document.getElementById('refresh-btn');
// Handle postal code form submission
if (postalForm) {
postalForm.addEventListener('submit', (event) => {
event.preventDefault();
const postalCode = document.getElementById('postal-code').value.trim();
if (postalCode) {
handlePostalCodeSubmission(postalCode);
}
});
}
// Handle refresh button
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
const postalCode = refreshBtn.dataset.postalCode || document.getElementById('postal-code').value.trim();
if (postalCode) {
handlePostalCodeSubmission(postalCode);
}
});
}
}
// Initialize everything when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
initializeRepresentativesMap();
initializePostalForm();
});
// Global function for opening email modal from map popups
window.openEmailModal = function(email, name, level) {
if (window.emailComposer) {
window.emailComposer.openModal({
email: email,
name: name,
level: level
}, currentPostalCode);
}
};
// Export functions for use by other modules
window.RepresentativesMap = {
displayRepresentativeOffices,
initializeRepresentativesMap,
clearRepresentativeMarkers,
handlePostalCodeSubmission
};