/** * 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: '
🏛️
', iconSize: [40, 40], iconAnchor: [20, 40], popupAnchor: [0, -40] }), provincial: L.divIcon({ className: 'office-marker provincial', html: '
🏢
', iconSize: [40, 40], iconAnchor: [20, 40], popupAnchor: [0, -40] }), municipal: L.divIcon({ className: 'office-marker municipal', html: '
🏛️
', 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: '© OpenStreetMap 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 ? '

📍 Shared office location with other representatives

' : ''; // If office has original coordinates, show actual address const addressDisplay = office.isOffset ? `

Address: ${office.address}

Note: Marker positioned nearby for visibility

` : office.address ? `

Address: ${office.address}

` : ''; return `
${representative.photo_url ? `${representative.name}` : ''}

${representative.name}

${level}

${representative.district_name || 'District not specified'}

${locationNote}
Office Information
${addressDisplay} ${office.phone ? `

Phone: ${office.phone}

` : ''} ${office.fax ? `

Fax: ${office.fax}

` : ''} ${office.postal_code ? `

Postal Code: ${office.postal_code}

` : ''}
${representative.email ? `` : ''}
`; }// 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 = `

${message}

`; 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 };