@@ -3,11 +3,13 @@ import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
33import { initializeMap , addDivisionLayers , loadDivisionData } from ' ./map/mapConfig'
44import { highlightMatchedDivisions , fitMapToFeatures } from ' ./map/divisionHighlighting'
55import { fetchDivisionStats , updateSourceWithCounts } from ' ./map/divisionStats'
6- import { debounce , divisionWithHyphen } from ' ./map/divisionUtils'
6+ import { debounce , divisionWithHyphen , findDivisionByCoordinates } from ' ./map/divisionUtils'
77import { createClient } from ' @supabase/supabase-js'
8+ import maplibregl from ' maplibre-gl'
89
910const mapContainer = ref (null )
1011let map = null
12+ let userMarker = null
1113
1214// Initialize Supabase client
1315const supabase = createClient (
@@ -40,39 +42,173 @@ const props = defineProps({
4042
4143const 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
77213watch (() => 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
155297onUnmounted (() => {
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 }
0 commit comments