freealberta/influence/app/public/js/representatives-map.js
2025-10-16 10:44:49 -06:00

925 lines
35 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
async 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);
// Show geocoding progress
showMapMessage(`Locating ${representatives.length} office${representatives.length > 1 ? 's' : ''}...`);
// Group representatives by office location to handle shared addresses
const locationGroups = new Map();
// Process all representatives and geocode their offices
for (const rep of representatives) {
console.log(`Processing representative:`, rep.name, rep.representative_set_name);
// Get office location (now async for geocoding)
const offices = await 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);
}
});
}
// Clear the loading message
const mapContainer = document.getElementById('main-map');
const existingMessage = mapContainer?.querySelector('.map-message');
if (existingMessage) {
existingMessage.remove();
}
// Create markers for each location group
let offsetIndex = 0;
locationGroups.forEach((locationGroup, locationKey) => {
const numReps = locationGroup.representatives.length;
console.log(`Creating markers for location ${locationKey} with ${numReps} representatives`);
if (numReps === 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 in a circle
locationGroup.representatives.forEach((rep, repIndex) => {
const office = locationGroup.offices[repIndex];
// Increase offset distance based on number of representatives
// More reps = larger circle for better visibility
const baseDistance = 0.001; // About 100 meters base
const offsetDistance = baseDistance * (1 + (numReps / 10)); // Scale with count
// Arrange in a circle around the point
const angle = (repIndex * 2 * Math.PI) / numReps;
const offsetLat = office.lat + (offsetDistance * Math.cos(angle));
const offsetLng = office.lng + (offsetDistance * Math.sin(angle));
const offsetOffice = {
...office,
lat: offsetLat,
lng: offsetLng,
isOffset: true,
originalLat: office.lat,
originalLng: office.lng
};
console.log(`Creating offset marker ${repIndex + 1}/${numReps} for ${rep.name} at ${offsetLat}, ${offsetLng} (offset from ${office.lat}, ${office.lng})`);
const marker = createOfficeMarker(rep, offsetOffice, true);
if (marker) {
representativeMarkers.push(marker);
marker.addTo(representativesMap);
bounds.push([offsetLat, offsetLng]);
}
});
// Add the original center point to bounds as well
bounds.push([locationGroup.lat, locationGroup.lng]);
}
});
console.log(`Total markers created: ${representativeMarkers.length}`);
console.log(`Unique locations: ${locationGroups.size}`);
console.log(`Bounds array:`, bounds);
// Log summary of locations
const locationSummary = [];
locationGroups.forEach((group, key) => {
locationSummary.push({
location: key,
address: group.address.substring(0, 50) + '...',
representatives: group.representatives.map(r => r.name).join(', ')
});
});
console.table(locationSummary);
// 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
async 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)) {
for (const office of representative.offices) {
console.log(`Processing office:`, 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, try to geocode the address
for (const office of offices) {
if (!office.lat || !office.lng) {
console.log(`Geocoding address for ${representative.name}: ${office.address}`);
// Try geocoding the actual address first
const geocoded = await geocodeWithRateLimit(office.address);
if (geocoded) {
office.lat = geocoded.lat;
office.lng = geocoded.lng;
console.log('Geocoded office:', office);
} else {
// Fallback to city-level approximation
console.log(`Geocoding failed, using city approximation for ${representative.name}`);
const approxLocation = getApproximateLocationByDistrict(
representative.district_name,
representative.representative_set_name,
office.address
);
console.log('Approximate location:', approxLocation);
if (approxLocation) {
office.lat = approxLocation.lat;
office.lng = approxLocation.lng;
console.log('Updated office with approximate 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}`);
// For fallback, try to get a better location based on district
const approxLocation = getApproximateLocationByDistrict(
representative.district_name,
representative.representative_set_name,
null // No address available for fallback
);
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;
}
// Geocoding cache to avoid repeated API calls
const geocodingCache = new Map();
// Clean and normalize address for geocoding
function normalizeAddressForGeocoding(address) {
if (!address) return '';
// Special handling for well-known government buildings
const lowerAddress = address.toLowerCase();
// Handle House of Commons / Parliament
if (lowerAddress.includes('house of commons') || lowerAddress.includes('parliament')) {
if (lowerAddress.includes('ottawa') || lowerAddress.includes('k1a')) {
return 'Parliament Hill, Ottawa, ON, Canada';
}
}
// Handle Alberta Legislature
if (lowerAddress.includes('legislature') && (lowerAddress.includes('edmonton') || lowerAddress.includes('alberta'))) {
return '10800 97 Avenue NW, Edmonton, AB, Canada';
}
// Split by newlines
const lines = address.split('\n').map(line => line.trim()).filter(line => line);
// Remove lines that are just metadata/descriptive text
const filteredLines = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const lower = line.toLowerCase();
// Skip pure descriptive prefixes without addresses
if (lower.match(/^(main|local|district|constituency|legislature|regional)\s+(office|bureau)\s*-?\s*$/i)) {
continue; // Skip "Main office -" or "Constituency office" on their own
}
// Skip lines that are just "Main office - City" with no street address
if (lower.match(/^(main|local)\s+office\s*-\s*[a-z\s]+$/i) && !lower.match(/\d+/)) {
continue; // Skip if no street number
}
// Skip "Office:" prefixes
if (lower.match(/^office:\s*$/i)) {
continue;
}
// For lines starting with floor/suite/unit, try to extract just the street address
let cleanLine = line;
// Remove floor/suite/unit prefixes: "6th Floor, 123 Main St" -> "123 Main St"
cleanLine = cleanLine.replace(/^(suite|unit|floor|room|\d+(st|nd|rd|th)\s+floor)\s*,?\s*/i, '');
// Remove unit numbers at start: "#201, 123 Main St" -> "123 Main St"
cleanLine = cleanLine.replace(/^(#|unit|suite|ste\.?|apt\.?)\s*\d+[a-z]?\s*,\s*/i, '');
// Remove building names that precede addresses: "City Hall, 1 Main St" -> "1 Main St"
cleanLine = cleanLine.replace(/^(city hall|legislature building|federal building|provincial building),\s*/i, '');
// Clean up common building name patterns if there's a street address following
if (i === 0 && lines.length > 1) {
// If first line is just a building name and we have more lines, skip it
if (lower.match(/^(city hall|legislature|parliament|house of commons)$/i)) {
continue;
}
}
// Add the cleaned line if it has substance (contains a number for street address)
if (cleanLine.trim() && (cleanLine.match(/\d/) || cleanLine.match(/(edmonton|calgary|ottawa|alberta)/i))) {
filteredLines.push(cleanLine.trim());
}
}
// If we filtered everything, try a more lenient approach
if (filteredLines.length === 0) {
// Just join all lines and do basic cleanup
return lines
.map(line => line.replace(/^(main|local|district|constituency)\s+(office\s*-?\s*)/i, ''))
.filter(line => line.trim())
.join(', ') + ', Canada';
}
// Build cleaned address
let cleanAddress = filteredLines.join(', ');
// Fix Edmonton-style addresses: "9820 - 107 Street" -> "9820 107 Street"
cleanAddress = cleanAddress.replace(/(\d+)\s*-\s*(\d+\s+(Street|Avenue|Ave|St|Road|Rd|Drive|Dr|Boulevard|Blvd|Way|Lane|Ln))/gi, '$1 $2');
// Ensure it ends with "Canada" for better geocoding
if (!cleanAddress.toLowerCase().includes('canada')) {
cleanAddress += ', Canada';
}
return cleanAddress;
}
// Geocode an address using our backend API (which proxies to Nominatim)
async function geocodeAddress(address) {
// Check cache first
const cacheKey = address.toLowerCase().trim();
if (geocodingCache.has(cacheKey)) {
console.log(`Using cached coordinates for: ${address}`);
return geocodingCache.get(cacheKey);
}
try {
// Clean and normalize the address for better geocoding
const cleanedAddress = normalizeAddressForGeocoding(address);
console.log(`Original address: ${address}`);
console.log(`Cleaned address for geocoding: ${cleanedAddress}`);
// Call our backend geocoding endpoint
const response = await fetch('/api/geocode', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ address: cleanedAddress })
});
if (!response.ok) {
console.warn(`Geocoding API error: ${response.status}`);
return null;
}
const data = await response.json();
if (data.success && data.data && data.data.lat && data.data.lng) {
const coords = {
lat: data.data.lat,
lng: data.data.lng
};
console.log(`✓ Geocoded "${cleanedAddress}" to:`, coords);
console.log(` Display name: ${data.data.display_name}`);
// Cache the result using the original address as key
geocodingCache.set(cacheKey, coords);
return coords;
} else {
console.log(`✗ No geocoding results for: ${cleanedAddress}`);
return null;
}
} catch (error) {
console.error(`Geocoding error for "${address}":`, error);
return null;
}
}
// Rate limiter for geocoding requests (Nominatim has a 1 request/second limit)
let lastGeocodeTime = 0;
const GEOCODE_DELAY = 1100; // 1.1 seconds between requests
async function geocodeWithRateLimit(address) {
const now = Date.now();
const timeSinceLastRequest = now - lastGeocodeTime;
if (timeSinceLastRequest < GEOCODE_DELAY) {
const waitTime = GEOCODE_DELAY - timeSinceLastRequest;
console.log(`Rate limiting: waiting ${waitTime}ms before geocoding`);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
lastGeocodeTime = Date.now();
return await geocodeAddress(address);
}
// Alberta city coordinates lookup table (used as fallback)
const albertaCityCoordinates = {
// Major cities
'Edmonton': { lat: 53.5461, lng: -113.4938 },
'Calgary': { lat: 51.0447, lng: -114.0719 },
'Red Deer': { lat: 52.2681, lng: -113.8111 },
'Lethbridge': { lat: 49.6942, lng: -112.8328 },
'Medicine Hat': { lat: 50.0408, lng: -110.6775 },
'Grande Prairie': { lat: 55.1708, lng: -118.7947 },
'Airdrie': { lat: 51.2917, lng: -114.0144 },
'Fort McMurray': { lat: 56.7267, lng: -111.3790 },
'Spruce Grove': { lat: 53.5450, lng: -113.9006 },
'Okotoks': { lat: 50.7251, lng: -113.9778 },
'Leduc': { lat: 53.2594, lng: -113.5517 },
'Lloydminster': { lat: 53.2782, lng: -110.0053 },
'Camrose': { lat: 53.0167, lng: -112.8233 },
'Brooks': { lat: 50.5644, lng: -111.8986 },
'Cold Lake': { lat: 54.4639, lng: -110.1825 },
'Wetaskiwin': { lat: 52.9692, lng: -113.3769 },
'Stony Plain': { lat: 53.5267, lng: -114.0069 },
'Sherwood Park': { lat: 53.5344, lng: -113.3169 },
'St. Albert': { lat: 53.6303, lng: -113.6258 },
'Beaumont': { lat: 53.3572, lng: -113.4147 },
'Cochrane': { lat: 51.1942, lng: -114.4686 },
'Canmore': { lat: 51.0886, lng: -115.3581 },
'Banff': { lat: 51.1784, lng: -115.5708 },
'Jasper': { lat: 52.8737, lng: -118.0814 },
'Hinton': { lat: 53.4053, lng: -117.5856 },
'Whitecourt': { lat: 54.1433, lng: -115.6856 },
'Slave Lake': { lat: 55.2828, lng: -114.7728 },
'High River': { lat: 50.5792, lng: -113.8744 },
'Strathmore': { lat: 51.0364, lng: -113.4006 },
'Chestermere': { lat: 51.0506, lng: -113.8228 },
'Fort Saskatchewan': { lat: 53.7103, lng: -113.2192 },
'Lacombe': { lat: 52.4678, lng: -113.7372 },
'Sylvan Lake': { lat: 52.3081, lng: -114.0958 },
'Taber': { lat: 49.7850, lng: -112.1508 },
'Drayton Valley': { lat: 53.2233, lng: -114.9819 },
'Westlock': { lat: 54.1508, lng: -113.8631 },
'Ponoka': { lat: 52.6772, lng: -113.5836 },
'Morinville': { lat: 53.8022, lng: -113.6497 },
'Vermilion': { lat: 53.3553, lng: -110.8583 },
'Drumheller': { lat: 51.4633, lng: -112.7086 },
'Peace River': { lat: 56.2364, lng: -117.2892 },
'High Prairie': { lat: 55.4358, lng: -116.4856 },
'Athabasca': { lat: 54.7192, lng: -113.2856 },
'Bonnyville': { lat: 54.2681, lng: -110.7431 },
'Vegreville': { lat: 53.4944, lng: -112.0494 },
'Innisfail': { lat: 52.0358, lng: -113.9503 },
'Provost': { lat: 52.3547, lng: -110.2681 },
'Olds': { lat: 51.7928, lng: -114.1064 },
'Pincher Creek': { lat: 49.4858, lng: -113.9506 },
'Cardston': { lat: 49.1983, lng: -113.3028 },
'Crowsnest Pass': { lat: 49.6372, lng: -114.4831 },
// Capital references
'Ottawa': { lat: 45.4215, lng: -75.6972 }, // For federal legislature offices
'AB': { lat: 53.9333, lng: -116.5765 } // Alberta center
};
// Parse city from office address
function parseCityFromAddress(addressString) {
if (!addressString) return null;
// Common patterns in addresses
const lines = addressString.split('\n').map(line => line.trim()).filter(line => line);
// Check each line for city names
for (const line of lines) {
// Look for city names in our lookup table
for (const city in albertaCityCoordinates) {
if (line.includes(city)) {
console.log(`Found city "${city}" in address line: "${line}"`);
return city;
}
}
// Check for "City, Province" pattern
const cityProvinceMatch = line.match(/^([^,]+),\s*(AB|Alberta)/i);
if (cityProvinceMatch) {
const cityName = cityProvinceMatch[1].trim();
console.log(`Extracted city from province pattern: "${cityName}"`);
// Try to find this in our lookup
for (const city in albertaCityCoordinates) {
if (cityName.toLowerCase().includes(city.toLowerCase()) ||
city.toLowerCase().includes(cityName.toLowerCase())) {
return city;
}
}
}
}
return null;
}
// Get approximate location based on office address, district, and government level
function getApproximateLocationByDistrict(district, level, officeAddress = null) {
console.log(`Getting approximate location for district: ${district}, level: ${level}, address: ${officeAddress}`);
// First, try to parse city from office address
if (officeAddress) {
const city = parseCityFromAddress(officeAddress);
if (city && albertaCityCoordinates[city]) {
console.log(`Using coordinates for city: ${city}`);
return albertaCityCoordinates[city];
}
}
// Try to extract city from district name
if (district) {
// Check if district contains a city name
for (const city in albertaCityCoordinates) {
if (district.includes(city)) {
console.log(`Found city "${city}" in district name: "${district}"`);
return albertaCityCoordinates[city];
}
}
}
// Fallback based on government level and typical office locations
const levelLocations = {
'House of Commons': albertaCityCoordinates['Ottawa'], // Federal = Ottawa
'Legislative Assembly of Alberta': { lat: 53.5344, lng: -113.5065 }, // Provincial = Legislature
'Edmonton City Council': { lat: 53.5444, lng: -113.4909 } // Municipal = City Hall
};
if (level && levelLocations[level]) {
console.log(`Using level-based location for: ${level}`);
return levelLocations[level];
}
// Last resort: Alberta center
console.log('Using default Alberta center location');
return albertaCityCoordinates['AB'];
}
// 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(' ', '-');
// Show note if this is an offset marker at a shared location
const locationNote = isSharedLocation
? '<p class="shared-location-note"><small><em>📍 Shared office location with other representatives</em></small></p>'
: '';
// If office has original coordinates, show actual address
const addressDisplay = office.isOffset
? `<p><strong>Address:</strong> ${office.address}</p><p><small><em>Note: Marker positioned nearby for visibility</em></small></p>`
: office.address ? `<p><strong>Address:</strong> ${office.address}</p>` : '';
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>
${locationNote}
</div>
</div>
<div class="office-details">
<h5>Office Information</h5>
${addressDisplay}
${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();
if (data.success && data.data && data.data.representatives) {
// Display representatives on map (now async for geocoding)
await displayRepresentativeOffices(data.data.representatives, normalizedPostalCode);
hideLoading();
// 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 {
hideLoading();
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
};