From ffb09a01f8d3d3d3208657170b9a02d6ed61ca82 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 16 Oct 2025 10:44:49 -0600 Subject: [PATCH] geocoding fixes --- influence/GEOCODING_DEBUG.md | 84 ++++ influence/app/controllers/representatives.js | 48 ++ influence/app/public/css/styles.css | 456 +++++++++++++++++- influence/app/public/index.html | 31 +- .../app/public/js/representatives-display.js | 42 +- .../app/public/js/representatives-map.js | 454 ++++++++++++++--- influence/app/routes/api.js | 11 + 7 files changed, 1049 insertions(+), 77 deletions(-) create mode 100644 influence/GEOCODING_DEBUG.md diff --git a/influence/GEOCODING_DEBUG.md b/influence/GEOCODING_DEBUG.md new file mode 100644 index 0000000..c51f4d4 --- /dev/null +++ b/influence/GEOCODING_DEBUG.md @@ -0,0 +1,84 @@ +# Geocoding Debug Guide + +## How the Geocoding System Works + +The map now uses **real geocoding** via the Nominatim API (OpenStreetMap) to get precise coordinates for office addresses. + +### Process Flow: + +1. **Address Normalization**: Cleans addresses by removing metadata like "Main office", "2nd Floor", etc. +2. **Geocoding**: Sends cleaned address to Nominatim API +3. **Caching**: Stores geocoded coordinates to avoid repeated API calls +4. **Rate Limiting**: Respects Nominatim's 1 request/second limit +5. **Marker Placement**: + - Single offices: placed at exact geocoded location + - Shared offices: spread in a circle around the location for visibility + +### Debugging in Browser Console + +After searching for a postal code, check the browser console (F12) for: + +```javascript +// You should see output like: +Original address: 2nd Floor, City Hall +1 Sir Winston Churchill Square +Edmonton AB T5J 2R7 + +Cleaned address for geocoding: 1 Sir Winston Churchill Square, Edmonton AB T5J 2R7, Canada + +✓ Geocoded "1 Sir Winston Churchill Square, Edmonton AB T5J 2R7, Canada" to: {lat: 53.5440376, lng: -113.4897656} + Display name: Sir Winston Churchill Square, 9918, Downtown, Central Core, Edmonton, Alberta, T5J 5H7, Canada +``` + +### Expected Behavior + +**For Edmonton postal codes (T5J, T5K, etc.):** +- Municipal reps → Should appear at City Hall (Sir Winston Churchill Square) +- Provincial MLAs → Should appear at their constituency offices (geocoded addresses) +- Federal MPs → May appear at Parliament Hill in Ottawa OR local Edmonton offices + +**For Calgary postal codes (T1Y, T2P, etc.):** +- Should appear at various Calgary locations based on constituency offices + +**For other Alberta cities:** +- Should appear at the actual street addresses in those cities + +### Why Some Clustering is Normal + +If you see multiple markers in the same area, it could be because: + +1. **Legitimately Shared Offices**: Multiple city councillors work from City Hall +2. **Same Building, Different Offices**: Legislature has multiple MLAs +3. **Geocoding to Building vs Street**: Some addresses geocode to the building center + +The system now **spreads these markers in a circle** around the shared location so you can click each one individually. + +### Testing Different Locations + +Try these postal codes to verify geographic diversity: + +- **Edmonton Downtown**: T5J 2R7 (should show City Hall area) +- **Calgary**: T1Y 1A1 (should show Calgary locations) +- **Red Deer**: T4N 1A1 (should show Red Deer locations) +- **Lethbridge**: T1J 0A1 (should show Lethbridge locations) + +### Geocoding Cache + +The system caches geocoding results in browser memory. To reset: +- Refresh the page (F5) +- Or run in console: `geocodingCache.clear()` + +### API Rate Limiting + +Nominatim allows 1 request per second. For 10 representatives with offices: +- Estimated time: 10-15 seconds to geocode all addresses +- Cached results are instant + +### Fallback Behavior + +If geocoding fails for an address, the system falls back to: +1. City-level coordinates (from Alberta cities lookup table) +2. District-based approximation +3. Government level default (Legislature, City Hall, etc.) + +This ensures every representative has a marker, even if precise geocoding fails. diff --git a/influence/app/controllers/representatives.js b/influence/app/controllers/representatives.js index 845889d..945c9f6 100644 --- a/influence/app/controllers/representatives.js +++ b/influence/app/controllers/representatives.js @@ -229,6 +229,54 @@ class RepresentativesController { }); } } + + async geocodeAddress(req, res, next) { + try { + const { address } = req.body; + const axios = require('axios'); + + console.log(`Geocoding address: ${address}`); + + // Use Nominatim API (OpenStreetMap) + const encodedAddress = encodeURIComponent(address); + const url = `https://nominatim.openstreetmap.org/search?format=json&q=${encodedAddress}&limit=1&countrycodes=ca`; + + const response = await axios.get(url, { + headers: { + 'User-Agent': 'BNKops-Influence-Tool/1.0' + }, + timeout: 5000 + }); + + if (response.data && response.data.length > 0 && response.data[0].lat && response.data[0].lon) { + const result = { + lat: parseFloat(response.data[0].lat), + lng: parseFloat(response.data[0].lon), + display_name: response.data[0].display_name + }; + + console.log(`Geocoded "${address}" to:`, result); + + res.json({ + success: true, + data: result + }); + } else { + console.log(`No geocoding results for: ${address}`); + res.json({ + success: false, + message: 'No results found for this address' + }); + } + } catch (error) { + console.error('Geocoding error:', error); + res.status(500).json({ + success: false, + error: 'Geocoding failed', + message: error.message + }); + } + } } module.exports = new RepresentativesController(); \ No newline at end of file diff --git a/influence/app/public/css/styles.css b/influence/app/public/css/styles.css index e7ffa27..472da1e 100644 --- a/influence/app/public/css/styles.css +++ b/influence/app/public/css/styles.css @@ -20,21 +20,456 @@ body { header { text-align: center; margin-bottom: 40px; - padding: 40px 20px; - background: white; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 60px 20px; + background: linear-gradient(135deg, #005a9c 0%, #007acc 50%, #0099ff 100%); + background-size: 200% 200%; + border-radius: 12px; + box-shadow: 0 8px 24px rgba(0, 90, 156, 0.2); + position: relative; + overflow: hidden; + animation: gradientShift 15s ease infinite; +} + +@keyframes gradientShift { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} + +.header-background { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; +} + +.gradient-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient(circle at 20% 50%, rgba(255, 255, 255, 0.1) 0%, transparent 50%); + animation: overlayPulse 8s ease-in-out infinite; +} + +@keyframes overlayPulse { + 0%, 100% { opacity: 0.3; } + 50% { opacity: 0.6; } +} + +.particles { + position: absolute; + width: 100%; + height: 100%; + overflow: hidden; +} + +.particle { + position: absolute; + font-size: 24px; + animation: float 20s infinite ease-in-out; + opacity: 0.4; + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2)); +} + +.particle:nth-child(1) { + left: 10%; + top: 20%; + animation-duration: 18s; + animation-delay: 0s; + font-size: 28px; +} + +.particle:nth-child(2) { + left: 80%; + top: 60%; + animation-duration: 22s; + animation-delay: 2s; + font-size: 22px; +} + +.particle:nth-child(3) { + left: 30%; + top: 70%; + animation-duration: 20s; + animation-delay: 4s; + font-size: 26px; +} + +.particle:nth-child(4) { + left: 60%; + top: 30%; + animation-duration: 19s; + animation-delay: 1s; + font-size: 24px; +} + +.particle:nth-child(5) { + left: 90%; + top: 80%; + animation-duration: 21s; + animation-delay: 3s; + font-size: 25px; +} + +.particle:nth-child(6) { + left: 50%; + top: 10%; + animation-duration: 17s; + animation-delay: 5s; + font-size: 23px; +} + +.particle:nth-child(7) { + left: 15%; + top: 50%; + animation-duration: 23s; + animation-delay: 6s; + font-size: 27px; +} + +.particle:nth-child(8) { + left: 75%; + top: 15%; + animation-duration: 19s; + animation-delay: 2.5s; + font-size: 24px; +} + +@keyframes float { + 0%, 100% { + transform: translate(0, 0) scale(1) rotate(0deg); + opacity: 0.3; + } + 25% { + transform: translate(30px, -30px) scale(1.2) rotate(15deg); + opacity: 0.5; + } + 50% { + transform: translate(-20px, 20px) scale(0.9) rotate(-10deg); + opacity: 0.4; + } + 75% { + transform: translate(20px, 30px) scale(1.1) rotate(20deg); + opacity: 0.45; + } +} + +/* Unified Header Section */ +.unified-header-section { + position: relative; + background: linear-gradient(135deg, #005a9c 0%, #007acc 50%, #0099ff 100%); + background-size: 200% 200%; + animation: gradientShift 15s ease infinite; + border-radius: 12px; + padding: 60px 20px 40px; + margin-bottom: 40px; + box-shadow: 0 8px 24px rgba(0, 90, 156, 0.2); + overflow: hidden; + text-align: center; +} + +@keyframes gradientShift { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} + +.section-background { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 0; +} + +.gradient-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient(circle at 20% 50%, rgba(255, 255, 255, 0.1) 0%, transparent 50%); + animation: overlayPulse 8s ease-in-out infinite; +} + +@keyframes overlayPulse { + 0%, 100% { opacity: 0.3; } + 50% { opacity: 0.6; } +} + +.particles { + position: absolute; + width: 100%; + height: 100%; + overflow: hidden; +} + +.particle { + position: absolute; + font-size: 24px; + animation: float 20s infinite ease-in-out; + opacity: 0.4; + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2)); +} + +.particle:nth-child(1) { + left: 10%; + top: 20%; + animation-duration: 18s; + animation-delay: 0s; + font-size: 28px; +} + +.particle:nth-child(2) { + left: 80%; + top: 60%; + animation-duration: 22s; + animation-delay: 2s; + font-size: 22px; +} + +.particle:nth-child(3) { + left: 30%; + top: 70%; + animation-duration: 20s; + animation-delay: 4s; + font-size: 26px; +} + +.particle:nth-child(4) { + left: 60%; + top: 30%; + animation-duration: 19s; + animation-delay: 1s; + font-size: 24px; +} + +.particle:nth-child(5) { + left: 90%; + top: 80%; + animation-duration: 21s; + animation-delay: 3s; + font-size: 25px; +} + +.particle:nth-child(6) { + left: 50%; + top: 10%; + animation-duration: 17s; + animation-delay: 5s; + font-size: 23px; +} + +.particle:nth-child(7) { + left: 15%; + top: 50%; + animation-duration: 23s; + animation-delay: 6s; + font-size: 27px; +} + +.particle:nth-child(8) { + left: 75%; + top: 15%; + animation-duration: 19s; + animation-delay: 2.5s; + font-size: 24px; +} + +.particle:nth-child(9) { + left: 40%; + top: 40%; + animation-duration: 20s; + animation-delay: 4s; + font-size: 26px; +} + +.particle:nth-child(10) { + left: 65%; + top: 65%; + animation-duration: 18s; + animation-delay: 1.5s; + font-size: 25px; +} + +@keyframes float { + 0%, 100% { + transform: translate(0, 0) scale(1) rotate(0deg); + opacity: 0.3; + } + 25% { + transform: translate(30px, -30px) scale(1.2) rotate(15deg); + opacity: 0.5; + } + 50% { + transform: translate(-20px, 20px) scale(0.9) rotate(-10deg); + opacity: 0.4; + } + 75% { + transform: translate(20px, 30px) scale(1.1) rotate(20deg); + opacity: 0.45; + } +} + +.unified-header-section .header-content { + position: relative; + z-index: 1; + margin-bottom: 50px; +} + +.unified-header-section .header-content h1 { + color: #ffffff; + margin-bottom: 10px; + font-size: 2.5em; + text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + font-weight: 700; +} + +.unified-header-section .header-content p { + color: rgba(255, 255, 255, 0.95); + font-size: 1.1em; + text-shadow: 0 1px 5px rgba(0, 0, 0, 0.15); +} + +.brand-link { + color: #ffffff; + text-decoration: none; + position: relative; + display: inline-block; + transition: all 0.3s ease; +} + +.brand-link::after { + content: ''; + position: absolute; + width: 100%; + height: 2px; + bottom: -2px; + left: 0; + background-color: rgba(255, 255, 255, 0.6); + transform: scaleX(0); + transform-origin: bottom right; + transition: transform 0.3s ease; +} + +.brand-link:hover { + transform: translateY(-2px); +} + +.brand-link:hover::after { + transform: scaleX(1); + transform-origin: bottom left; +} + +.fade-in { + animation: fadeInUp 0.8s ease-out forwards; + opacity: 0; +} + +.fade-in-delay { + animation: fadeInUp 0.8s ease-out 0.3s forwards; + opacity: 0; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.unified-header-section .map-header { + position: relative; + z-index: 1; + margin-bottom: 30px; +} + +.unified-header-section .map-header h2 { + color: #ffffff; + text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + font-size: 2em; + font-weight: 600; + margin: 0; +} + +.header-content { + position: relative; + z-index: 1; } header h1 { - color: #005a9c; + color: #ffffff; margin-bottom: 10px; font-size: 2.5em; + text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + font-weight: 700; } header p { - color: #666; + color: rgba(255, 255, 255, 0.95); font-size: 1.1em; + text-shadow: 0 1px 5px rgba(0, 0, 0, 0.15); +} + +.brand-link { + color: #ffffff; + text-decoration: none; + position: relative; + display: inline-block; + transition: all 0.3s ease; +} + +.brand-link::after { + content: ''; + position: absolute; + width: 100%; + height: 2px; + bottom: -2px; + left: 0; + background-color: rgba(255, 255, 255, 0.6); + transform: scaleX(0); + transform-origin: bottom right; + transition: transform 0.3s ease; +} + +.brand-link:hover { + transform: translateY(-2px); +} + +.brand-link:hover::after { + transform: scaleX(1); + transform-origin: bottom left; +} + +.fade-in { + animation: fadeInUp 0.8s ease-out forwards; + opacity: 0; +} + +.fade-in-delay { + animation: fadeInUp 0.8s ease-out 0.3s forwards; + opacity: 0; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } } /* Buttons */ @@ -908,10 +1343,15 @@ footer a:hover { .postal-input-section { background: white; - padding: 20px; + padding: 30px; border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); margin-bottom: 20px; + max-width: 600px; + margin-left: auto; + margin-right: auto; + position: relative; + z-index: 1; } .postal-input-section .form-group { diff --git a/influence/app/public/index.html b/influence/app/public/index.html index a2631c3..442f34f 100644 --- a/influence/app/public/index.html +++ b/influence/app/public/index.html @@ -15,14 +15,31 @@
-
-

BNKops Influence Tool

-

Connect with your elected representatives across all levels of government

-
-
-
+
+
+
+
+ 🇨🇦 + 📧 + 📞 + ✉️ + 📱 + 🇨🇦 + 📧 + 📞 + 🇨🇦 + 📱 +
+
+ + +
+

BNKops Influence Tool

+

Connect with your elected representatives across all levels of government

+
+

Find Your Representatives

@@ -78,7 +95,7 @@
- +