583 lines
22 KiB
JavaScript
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: '© <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
|
|
}; |