Skip to content

Commit fee40d3

Browse files
feat: select user's current division on load
1 parent a14443c commit fee40d3

File tree

2 files changed

+202
-15
lines changed

2 files changed

+202
-15
lines changed

src/components/MapView.vue

Lines changed: 166 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
33
import { initializeMap, addDivisionLayers, loadDivisionData } from './map/mapConfig'
44
import { highlightMatchedDivisions, fitMapToFeatures } from './map/divisionHighlighting'
55
import { fetchDivisionStats, updateSourceWithCounts } from './map/divisionStats'
6-
import { debounce, divisionWithHyphen } from './map/divisionUtils'
6+
import { debounce, divisionWithHyphen, findDivisionByCoordinates } from './map/divisionUtils'
77
import { createClient } from '@supabase/supabase-js'
8+
import maplibregl from 'maplibre-gl'
89
910
const mapContainer = ref(null)
1011
let map = null
12+
let userMarker = null
1113
1214
// Initialize Supabase client
1315
const supabase = createClient(
@@ -40,39 +42,173 @@ const props = defineProps({
4042
4143
const emit = defineEmits(['update:searchResults'])
4244
43-
// Create debounced version of fitMapToFeatures
44-
const debouncedFitMapToFeatures = debounce((map, divisions, element) => {
45-
fitMapToFeatures(map, divisions, element)
46-
}, 300)
45+
// Create a custom marker element
46+
function createMarkerElement() {
47+
const wrapper = document.createElement('div');
48+
wrapper.style.position = 'relative';
49+
wrapper.style.width = '50px';
50+
wrapper.style.height = '50px';
51+
52+
// Create center dot
53+
const dot = document.createElement('div');
54+
dot.style.position = 'absolute';
55+
dot.style.left = '50%';
56+
dot.style.top = '50%';
57+
dot.style.transform = 'translate(-50%, -50%)';
58+
dot.style.width = '12px';
59+
dot.style.height = '12px';
60+
dot.style.backgroundColor = '#3388ff';
61+
dot.style.borderRadius = '50%';
62+
dot.style.border = '2px solid white';
63+
dot.style.boxShadow = '0 0 4px rgba(0,0,0,0.4)';
64+
wrapper.appendChild(dot);
65+
66+
// Create three pulse rings
67+
for (let i = 0; i < 3; i++) {
68+
const ring = document.createElement('div');
69+
ring.style.position = 'absolute';
70+
ring.style.left = '50%';
71+
ring.style.top = '50%';
72+
ring.style.transform = 'translate(-50%, -50%)';
73+
ring.style.width = '12px';
74+
ring.style.height = '12px';
75+
ring.style.borderRadius = '50%';
76+
ring.style.border = '2px solid #3388ff';
77+
ring.style.opacity = '0';
78+
ring.style.animation = `pulse${i + 1} 2s infinite`;
79+
wrapper.appendChild(ring);
80+
}
81+
82+
// Add keyframes for each ring
83+
if (!document.getElementById('marker-pulse-keyframes')) {
84+
const style = document.createElement('style');
85+
style.id = 'marker-pulse-keyframes';
86+
style.textContent = `
87+
@keyframes pulse1 {
88+
0% { width: 12px; height: 12px; opacity: 0.6; }
89+
100% { width: 40px; height: 40px; opacity: 0; }
90+
}
91+
@keyframes pulse2 {
92+
0% { width: 12px; height: 12px; opacity: 0; }
93+
33% { width: 12px; height: 12px; opacity: 0.6; }
94+
100% { width: 40px; height: 40px; opacity: 0; }
95+
}
96+
@keyframes pulse3 {
97+
0% { width: 12px; height: 12px; opacity: 0; }
98+
66% { width: 12px; height: 12px; opacity: 0.6; }
99+
100% { width: 40px; height: 40px; opacity: 0; }
100+
}
101+
`;
102+
document.head.appendChild(style);
103+
}
47104
48-
// Function to handle division clicks
49-
async function handleDivisionClick(division) {
50-
console.log('Division clicked:', division)
105+
return wrapper;
106+
}
107+
108+
// Function to get user's location
109+
async function getUserLocation() {
110+
return new Promise((resolve, reject) => {
111+
if (!navigator.geolocation) {
112+
reject(new Error('Geolocation is not supported by your browser'));
113+
return;
114+
}
115+
116+
navigator.geolocation.getCurrentPosition(
117+
(position) => {
118+
resolve({
119+
lng: position.coords.longitude,
120+
lat: position.coords.latitude
121+
});
122+
},
123+
(error) => {
124+
console.log('Geolocation error:', error.message);
125+
reject(error);
126+
},
127+
{
128+
enableHighAccuracy: true,
129+
timeout: 5000,
130+
maximumAge: 0
131+
}
132+
);
133+
});
134+
}
135+
136+
// Function to select and load division data
137+
async function selectDivision(division, location = null) {
138+
console.log('Selecting division:', division);
51139
try {
52140
const { data, error } = await supabase
53141
.from('phila_ballots')
54142
.select('name, division, id_number, birth_year, zip, ballot_status_reason')
55143
.eq('division', divisionWithHyphen(division))
56-
.limit(100)
144+
.limit(100);
57145
58146
if (error) {
59-
console.error('Supabase search error:', error)
60-
return
147+
console.error('Supabase search error:', error);
148+
return;
61149
}
62150
63-
console.log('Search results:', data)
151+
console.log('Search results:', data);
64152
65153
// Update search results which will automatically update the panel
66154
emit('update:searchResults', {
67155
matches: data,
68156
divisions: [division]
69-
})
157+
});
158+
159+
// If location is provided, add/update the marker
160+
if (location) {
161+
if (userMarker) {
162+
userMarker.remove();
163+
}
164+
const markerEl = createMarkerElement();
165+
userMarker = new maplibregl.Marker({
166+
element: markerEl,
167+
anchor: 'center'
168+
})
169+
.setLngLat([location.lng, location.lat])
170+
.addTo(map);
171+
}
70172
71173
} catch (err) {
72-
console.error('Search error:', err)
174+
console.error('Search error:', err);
73175
}
74176
}
75177
178+
// Function to highlight user's division
179+
async function highlightUserDivision() {
180+
try {
181+
const location = await getUserLocation();
182+
183+
// Check if coordinates are within Philadelphia bounds (rough estimate)
184+
const phillyBounds = {
185+
north: 40.1379,
186+
south: 39.8688,
187+
east: -74.9557,
188+
west: -75.2804
189+
};
190+
191+
if (location.lat < phillyBounds.south || location.lat > phillyBounds.north ||
192+
location.lng < phillyBounds.west || location.lng > phillyBounds.east) {
193+
console.log('User location is outside Philadelphia');
194+
return;
195+
}
196+
197+
// Find the division containing these coordinates
198+
const division = findDivisionByCoordinates(map, location);
199+
if (division) {
200+
await selectDivision(division, location);
201+
}
202+
} catch (error) {
203+
console.error('Error getting user location:', error);
204+
}
205+
}
206+
207+
// Create debounced version of fitMapToFeatures
208+
const debouncedFitMapToFeatures = debounce((map, divisions, element) => {
209+
fitMapToFeatures(map, divisions, element)
210+
}, 300)
211+
76212
// Watch for search results changes
77213
watch(() => props.searchResults, (newResults) => {
78214
console.log('Search results updated:', newResults)
@@ -117,6 +253,12 @@ onMounted(async () => {
117253
divisionStats.value = await fetchDivisionStats()
118254
updateSourceWithCounts(map, divisionStats.value)
119255
256+
// Wait for the style to be fully loaded before getting user location
257+
map.once('styledata', async () => {
258+
// Get and highlight user's division
259+
await highlightUserDivision()
260+
});
261+
120262
// Add hover interaction
121263
map.on('mousemove', 'divisions-fill', (e) => {
122264
if (e.features.length > 0) {
@@ -133,7 +275,7 @@ onMounted(async () => {
133275
map.on('click', 'divisions-fill', (e) => {
134276
if (e.features.length > 0) {
135277
const division = e.features[0].properties.DIVISION_NUM
136-
handleDivisionClick(division)
278+
selectDivision(division)
137279
}
138280
})
139281
@@ -153,6 +295,15 @@ onMounted(async () => {
153295
})
154296
155297
onUnmounted(() => {
298+
// Remove the keyframes style if it exists
299+
const keyframesStyle = document.getElementById('marker-pulse-keyframes');
300+
if (keyframesStyle) {
301+
keyframesStyle.remove();
302+
}
303+
304+
if (userMarker) {
305+
userMarker.remove()
306+
}
156307
if (map) {
157308
map.remove()
158309
}

src/components/map/divisionUtils.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,39 @@ export function debounce(fn, delay = 300) {
2929
timeoutId = setTimeout(() => fn.apply(this, args), delay)
3030
}
3131
}
32+
33+
// Find division that contains the given coordinates
34+
export function findDivisionByCoordinates(map, lngLat) {
35+
if (!map || !map.getSource('divisions') || !map.getSource('divisions')._data) {
36+
return null;
37+
}
38+
39+
const point = [lngLat.lng, lngLat.lat];
40+
const features = map.getSource('divisions')._data.features;
41+
42+
// Find the first division polygon that contains the point
43+
const containingFeature = features.find(feature => {
44+
if (feature.geometry.type !== 'Polygon') return false;
45+
return pointInPolygon(point, feature.geometry.coordinates[0]);
46+
});
47+
48+
return containingFeature ? containingFeature.properties.DIVISION_NUM : null;
49+
}
50+
51+
// Helper function to check if a point is inside a polygon
52+
function pointInPolygon(point, polygon) {
53+
const [x, y] = point;
54+
let inside = false;
55+
56+
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
57+
const [xi, yi] = polygon[i];
58+
const [xj, yj] = polygon[j];
59+
60+
const intersect = ((yi > y) !== (yj > y)) &&
61+
(x < (xj - xi) * (y - yi) / (yj - yi) + xi);
62+
63+
if (intersect) inside = !inside;
64+
}
65+
66+
return inside;
67+
}

0 commit comments

Comments
 (0)