geocoding fixes
This commit is contained in:
parent
4fb9847812
commit
ffb09a01f8
84
influence/GEOCODING_DEBUG.md
Normal file
84
influence/GEOCODING_DEBUG.md
Normal file
@ -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.
|
||||
@ -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();
|
||||
@ -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 {
|
||||
|
||||
@ -15,14 +15,31 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1><a href="https://bnkops.com/" target="_blank" style="color: inherit; text-decoration: underline;">BNKops</a> Influence Tool</h1>
|
||||
<p>Connect with your elected representatives across all levels of government</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- Postal Code Input Section -->
|
||||
<section id="postal-input-section">
|
||||
<section id="postal-input-section" class="unified-header-section">
|
||||
<div class="section-background">
|
||||
<div class="gradient-overlay"></div>
|
||||
<div class="particles">
|
||||
<span class="particle">🇨🇦</span>
|
||||
<span class="particle">📧</span>
|
||||
<span class="particle">📞</span>
|
||||
<span class="particle">✉️</span>
|
||||
<span class="particle">📱</span>
|
||||
<span class="particle">🇨🇦</span>
|
||||
<span class="particle">📧</span>
|
||||
<span class="particle">📞</span>
|
||||
<span class="particle">🇨🇦</span>
|
||||
<span class="particle">📱</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header Content -->
|
||||
<div class="header-content">
|
||||
<h1 class="fade-in"><a href="https://bnkops.com/" target="_blank" class="brand-link">BNKops</a> Influence Tool</h1>
|
||||
<p class="fade-in-delay">Connect with your elected representatives across all levels of government</p>
|
||||
</div>
|
||||
|
||||
<div class="map-header">
|
||||
<h2>Find Your Representatives</h2>
|
||||
</div>
|
||||
@ -78,7 +95,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Email Compose Modal -->
|
||||
<!-- epose Modal -->
|
||||
<div id="email-modal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
|
||||
@ -341,16 +341,46 @@ class RepresentativesDisplay {
|
||||
}
|
||||
|
||||
getShortAddress(address) {
|
||||
// Clean the address first
|
||||
const cleaned = this.cleanAddress(address);
|
||||
|
||||
// Extract city and province/state for short display
|
||||
const parts = address.split(',');
|
||||
const parts = cleaned.split(',').map(p => p.trim()).filter(p => p);
|
||||
|
||||
if (parts.length >= 2) {
|
||||
const city = parts[parts.length - 2].trim();
|
||||
const province = parts[parts.length - 1].trim();
|
||||
const city = parts[parts.length - 2];
|
||||
const province = parts[parts.length - 1];
|
||||
return `${city}, ${province}`;
|
||||
}
|
||||
|
||||
// Fallback: just show first part
|
||||
return parts[0].trim();
|
||||
// Fallback: just show the cleaned address
|
||||
return parts[0] || cleaned;
|
||||
}
|
||||
|
||||
cleanAddress(address) {
|
||||
if (!address) return '';
|
||||
|
||||
// Split by newlines and process each line
|
||||
const lines = address.split('\n').map(line => line.trim()).filter(line => line);
|
||||
|
||||
// Remove common prefixes and metadata
|
||||
const filteredLines = lines.filter(line => {
|
||||
const lower = line.toLowerCase();
|
||||
// Skip lines that are just descriptive text
|
||||
return !lower.startsWith('main office') &&
|
||||
!lower.startsWith('constituency office') &&
|
||||
!lower.startsWith('legislature office') &&
|
||||
!lower.startsWith('district office') &&
|
||||
!lower.startsWith('local office') &&
|
||||
!lower.startsWith('office:') &&
|
||||
!line.match(/^[a-z\s]+\s*-\s*/i); // Remove "Main office - City" patterns
|
||||
});
|
||||
|
||||
// If we filtered everything out, use original lines minus obvious prefixes
|
||||
const addressLines = filteredLines.length > 0 ? filteredLines : lines.slice(1);
|
||||
|
||||
// Join remaining lines with commas
|
||||
return addressLines.join(', ').trim();
|
||||
}
|
||||
|
||||
attachEventListeners() {
|
||||
@ -463,7 +493,7 @@ class RepresentativesDisplay {
|
||||
|
||||
handleVisitClick(address, name, office) {
|
||||
// Clean and format the address for URL encoding
|
||||
const cleanAddress = address.replace(/\n/g, ', ').trim();
|
||||
const cleanAddress = this.cleanAddress(address);
|
||||
|
||||
// Show confirmation dialog
|
||||
const message = `Open directions to ${name}'s office?\n\nAddress: ${cleanAddress}`;
|
||||
|
||||
@ -85,7 +85,7 @@ function clearRepresentativeMarkers() {
|
||||
}
|
||||
|
||||
// Add representative offices to the map
|
||||
function displayRepresentativeOffices(representatives, postalCode) {
|
||||
async function displayRepresentativeOffices(representatives, postalCode) {
|
||||
// Initialize map if not already done
|
||||
if (!representativesMap) {
|
||||
console.log('Map not initialized, initializing now...');
|
||||
@ -105,14 +105,18 @@ function displayRepresentativeOffices(representatives, postalCode) {
|
||||
|
||||
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();
|
||||
|
||||
representatives.forEach((rep, index) => {
|
||||
console.log(`Processing representative ${index + 1}:`, rep.name, rep.representative_set_name);
|
||||
// Process all representatives and geocode their offices
|
||||
for (const rep of representatives) {
|
||||
console.log(`Processing representative:`, rep.name, rep.representative_set_name);
|
||||
|
||||
// Try to get office location from various sources
|
||||
const offices = getOfficeLocations(rep);
|
||||
// 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) => {
|
||||
@ -139,14 +143,22 @@ function displayRepresentativeOffices(representatives, postalCode) {
|
||||
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) => {
|
||||
console.log(`Creating markers for location ${locationKey} with ${locationGroup.representatives.length} representatives`);
|
||||
const numReps = locationGroup.representatives.length;
|
||||
console.log(`Creating markers for location ${locationKey} with ${numReps} representatives`);
|
||||
|
||||
if (locationGroup.representatives.length === 1) {
|
||||
if (numReps === 1) {
|
||||
// Single representative at this location
|
||||
const rep = locationGroup.representatives[0];
|
||||
const office = locationGroup.offices[0];
|
||||
@ -157,36 +169,58 @@ function displayRepresentativeOffices(representatives, postalCode) {
|
||||
bounds.push([office.lat, office.lng]);
|
||||
}
|
||||
} else {
|
||||
// Multiple representatives at same location - create offset markers
|
||||
// Multiple representatives at same location - create offset markers in a circle
|
||||
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;
|
||||
// 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
|
||||
lng: offsetLng,
|
||||
isOffset: true,
|
||||
originalLat: office.lat,
|
||||
originalLng: office.lng
|
||||
};
|
||||
|
||||
console.log(`Creating offset marker for ${rep.name} at ${offsetLat}, ${offsetLng}`);
|
||||
const marker = createOfficeMarker(rep, offsetOffice, locationGroup.representatives.length > 1);
|
||||
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] });
|
||||
@ -200,7 +234,7 @@ function displayRepresentativeOffices(representatives, postalCode) {
|
||||
}
|
||||
|
||||
// Extract office locations from representative data
|
||||
function getOfficeLocations(representative) {
|
||||
async function getOfficeLocations(representative) {
|
||||
const offices = [];
|
||||
|
||||
console.log(`Getting office locations for ${representative.name}`);
|
||||
@ -208,8 +242,8 @@ function getOfficeLocations(representative) {
|
||||
|
||||
// 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);
|
||||
for (const office of representative.offices) {
|
||||
console.log(`Processing office:`, office);
|
||||
|
||||
// Use the 'postal' field which contains the address
|
||||
if (office.postal || office.address) {
|
||||
@ -225,28 +259,49 @@ function getOfficeLocations(representative) {
|
||||
console.log('Created office data:', officeData);
|
||||
offices.push(officeData);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// For all offices without coordinates, add approximate coordinates
|
||||
offices.forEach(office => {
|
||||
// For all offices without coordinates, try to geocode the address
|
||||
for (const office of offices) {
|
||||
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);
|
||||
console.log(`Geocoding address for ${representative.name}: ${office.address}`);
|
||||
|
||||
if (approxLocation) {
|
||||
office.lat = approxLocation.lat;
|
||||
office.lng = approxLocation.lng;
|
||||
console.log('Updated office with coordinates:', office);
|
||||
// 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}`);
|
||||
const approxLocation = getApproximateLocationByDistrict(representative.district_name, representative.representative_set_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) {
|
||||
@ -265,32 +320,308 @@ function getOfficeLocations(representative) {
|
||||
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 }
|
||||
};
|
||||
// Geocoding cache to avoid repeated API calls
|
||||
const geocodingCache = new Map();
|
||||
|
||||
// Try specific district first
|
||||
if (district && edmontonLocations[district]) {
|
||||
return edmontonLocations[district];
|
||||
// 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';
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback based on government level
|
||||
// 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': { 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
|
||||
'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
|
||||
};
|
||||
|
||||
return levelLocations[level] || { lat: 53.9333, lng: -116.5765 }; // Default to Alberta center
|
||||
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
|
||||
@ -326,6 +657,16 @@ function createOfficePopupContent(representative, office, isSharedLocation = fal
|
||||
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}">
|
||||
@ -334,13 +675,13 @@ function createOfficePopupContent(representative, office, isSharedLocation = fal
|
||||
<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>' : ''}
|
||||
${locationNote}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="office-details">
|
||||
<h5>Office Information</h5>
|
||||
${office.address ? `<p><strong>Address:</strong> ${office.address}</p>` : ''}
|
||||
${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>` : ''}
|
||||
@ -437,11 +778,11 @@ async function handlePostalCodeSubmission(postalCode) {
|
||||
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);
|
||||
// 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) {
|
||||
@ -495,6 +836,7 @@ async function handlePostalCodeSubmission(postalCode) {
|
||||
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) {
|
||||
|
||||
@ -45,6 +45,17 @@ router.post(
|
||||
representativesController.refreshPostalCode
|
||||
);
|
||||
|
||||
// Geocoding endpoint (proxy to Nominatim)
|
||||
router.post(
|
||||
'/geocode',
|
||||
rateLimiter.general,
|
||||
[
|
||||
body('address').notEmpty().withMessage('Address is required')
|
||||
],
|
||||
handleValidationErrors,
|
||||
representativesController.geocodeAddress
|
||||
);
|
||||
|
||||
// Email endpoints
|
||||
router.post(
|
||||
'/emails/preview',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user