File size: 16,193 Bytes
7f53bc2
 
4c8b4d5
1f2416b
fc8894f
346db32
2b5f454
f303655
2b5f454
ae1873a
 
32308ef
ae1873a
2b5f454
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
09b12dc
 
 
 
 
 
 
 
 
6e4de3c
 
 
 
ae1873a
 
 
 
 
 
 
 
 
 
 
1f2416b
ddfb219
f91d245
0cffa94
7aa3d69
 
 
 
 
 
 
d0c9a6e
7aa3d69
 
0cffa94
7aa3d69
 
 
 
 
 
 
 
 
dd2c1ff
7aa3d69
 
 
3f8e536
7aa3d69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f008966
7aa3d69
 
 
b5edb7c
7aa3d69
 
 
b5edb7c
7aa3d69
 
 
1f70408
7aa3d69
 
 
 
 
1f70408
7aa3d69
 
d98e4ef
7aa3d69
b5edb7c
7aa3d69
 
 
 
 
 
 
 
b5edb7c
607a134
 
 
 
 
 
 
24b9639
 
 
607a134
 
 
e330d29
607a134
 
 
 
 
b20cba3
ad4fac2
bfbcf7a
39a2183
bfbcf7a
 
 
 
 
5fedc81
f008966
39a2183
b5edb7c
 
 
 
 
 
3f8e536
f838ca7
b5edb7c
9c22ed4
 
32308ef
b5edb7c
 
 
 
 
 
 
 
 
cc873f2
9c22ed4
 
b5edb7c
 
 
 
 
 
 
 
24b9639
 
 
b5edb7c
24b9639
b5edb7c
24b9639
 
6c6ae46
24b9639
 
6c6ae46
24b9639
 
 
1f63ae9
24b9639
 
 
 
 
 
 
 
 
2004733
24b9639
2004733
13e3202
ddfb219
24b9639
 
 
62d34d5
2004733
 
24b9639
2004733
24b9639
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2b5f454
 
 
e426419
 
e9694f1
e426419
 
 
 
 
 
 
7145a3d
 
e426419
24b9639
 
 
 
 
 
e913fba
24b9639
 
00ce247
005dfdc
 
9162194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2d36ab1
24b9639
 
17b4f07
edbe9e1
24b9639
698c8f7
edbe9e1
e6ce543
4cdc7b6
dcdae15
 
e89993d
4cdc7b6
dcdae15
 
b2a70cb
2516614
 
005be17
c0ae0b4
8f11160
375f962
005be17
4cdc7b6
e19c0bb
9652bfc
df10d63
 
 
9652bfc
 
df10d63
9652bfc
a66d277
df10d63
9652bfc
 
df10d63
9652bfc
a120c2a
ae1873a
 
 
09b12dc
ccf9308
 
 
dbbca92
 
ccf9308
 
dbbca92
 
 
5dd67f0
ae1873a
3ff61a8
46ca8b1
1d62100
edbe9e1
39b56f5
67a935e
edbe9e1
 
 
 
 
b5010df
edbe9e1
 
 
c888109
edbe9e1
b5010df
39b56f5
 
 
 
 
24b9639
b5010df
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
import streamlit as st
import json
import pandas as pd
import requests
import os
import math

from openai import OpenAI

import folium
from streamlit_folium import folium_static
from twilio.rest import Client

from datetime import datetime
from datetime import time
from zoneinfo import ZoneInfo

timezone = ZoneInfo('America/Los_Angeles')

def get_time_score(current_datetime, shelter):
    current_day = current_datetime.strftime("%A")
    
    if current_day not in shelter['Days']:
        return 1

    weekday = current_datetime.weekday()

    current_hour = current_datetime.strftime("%H")
    current_minute = current_datetime.strftime("%M")
    current_time = time(int(current_hour), int(current_minute))

    hour_start = shelter['Hour Start'].split(',')
    minute_start = shelter['Minute Start'].split(',')
    shelter_start = time(int(hour_start[weekday]), int(minute_start[weekday]))

    hour_end = shelter['Hour End'].split(',')
    minute_end = shelter['Minute End'].split(',')
    shelter_end = time(int(hour_end[weekday]), int(minute_end[weekday]))

    if shelter_start < shelter_end:
        if shelter_start <= current_time <= shelter_end: return 0
        else: return 1
    else:
        if current_time >= shelter_start or current_time <= shelter_end: return 0
        else: return 1

def geocode_address(address, api_key):
    # URL encode the address
    encoded_address = requests.utils.quote(address)

    # Send a request to the Google Maps Geocoding API
    geocode_url = f"https://maps.googleapis.com/maps/api/geocode/json?address={encoded_address}&key={api_key}"
    response = requests.get(geocode_url)
    data = response.json()

    lat = data['results'][0]['geometry']['location']['lat']
    lon = data['results'][0]['geometry']['location']['lng']
    return round(lat, 6), round(lon, 6)

# Reference: https://github.com/sfc38/Google-Maps-API-Streamlit-App/blob/master/google_maps_app.py#L126-L135
def create_map():
    # Create the map with Google Maps
    map_obj = folium.Map(tiles=None)
    folium.TileLayer("https://{s}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}", 
                     attr="google", 
                     name="Google Maps", 
                     overlay=True, 
                     control=True, 
                     subdomains=["mt0", "mt1", "mt2", "mt3"]).add_to(map_obj)
    return map_obj

def call_gpt(user_needs, shelter_services, api_key):
    client = OpenAI(api_key = api_key)

    completion = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": "Given two variables 'user needs' (the ideal qualities/services of a shelter) and 'shelter services' (the services offered by a shelter), return an integer 0-10 that scores how well the 'shelter services' match the 'user needs' where 0 is the best fit and 10 is the worst fit. IMPORTANT: NO MATTER WHAT, ONLY RETURN THE INTEGER (NO EXTRA WORDS, PUNCTUATION, ETC.)"},
            {"role": "user", "content": f"user_needs: {user_needs}, shelter_services: {shelter_services}"}
        ]
    )

    score = completion.choices[0].message.content.strip()
    return int(score)

def get_urgency_score(user, shelter):
    if user == "Today": 
        if shelter == "Immidiate": return 0
        if shelter == "High": return 0.75
        if shelter == "Moderate": return 1
    elif user == "In the next few days":
        if shelter == "Immidiate": return 0.25
        if shelter == "High": return 0
        if shelter == "Moderate": return 0.75
    elif user == "In a week or more":
        if shelter == "Immidiate": return 0.75
        if shelter == "High": return 0.25
        if shelter == "Moderate": return 0

def get_duration_score(user, shelter):
    if user == "Overnight":
        if shelter == "Overnight": return 0
        if shelter == "Temporary": return 0.5
        if shelter == "Transitional": return 0.75
        if shelter == "Long-Term": return 1
    elif user == "A month or less":
        if shelter == "Overnight": return 0.5
        if shelter == "Temporary": return 0
        if shelter == "Transitional": return 0.25
        if shelter == "Long-Term": return 0.75
    elif user == "A couple of months":
        if shelter == "Overnight": return 0.75
        if shelter == "Temporary": return 0.25
        if shelter == "Transitional": return 0
        if shelter == "Long-Term": return 0.5
    elif user == "A year or more":
        if shelter == "Overnight": return 1
        if shelter == "Temporary": return 0.75
        if shelter == "Transitional": return 0.5
        if shelter == "Long-Term": return 0
    
def get_coordinates(zipcode: str, api_key: str) -> list:
    """
    Get the coordinates (latitude and longitude) of an address using the OpenWeather Geocoding API.

    Parameters:
    zipcode (str): The zipcode to geocode.
    api_key (str): Your OpenWeather API key.

    Returns:
    list: A list containing the latitude and longitude of the address.
    """

    base_url = "http://api.openweathermap.org/geo/1.0/zip"
    params = {
        'zip': str(zipcode) + ",US",
        'appid': api_key
    }

    response = requests.get(base_url, params=params)
    data = response.json()
    print(data)
    return [data.get('lat'), data.get('lon')]

def haversine(lat1, lon1, lat2, lon2):
    R = 6371  # Earth radius in kilometers. Use 3956 for miles.
    dlat = math.radians(lat2 - lat1)
    dlon = math.radians(lon2 - lon1)
    a = math.sin(dlat / 2) ** 2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon / 2) ** 2
    c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
    distance = R * c
    return distance

# Initialize session state
if 'form_submitted' not in st.session_state:
    st.session_state.form_submitted = False

if 'shelter_index' not in st.session_state:
    st.session_state.shelter_index = 0

if 'shelters_filtered' not in st.session_state:
    st.session_state.shelters_filtered = False

# Page config
st.set_page_config(
    page_title="ShelterSearch",
    layout="wide",
)

st.title("ShelterSearch")

if not st.session_state.form_submitted:
    st.write("Hello there! Fill out this quick form to receive recommendation for where you can go to receive help.")
    st.markdown("Please give us feedback at this [link](https://forms.gle/oLMJ2qVc6HYgwfCw9)")

    # should be updated manually annually - use zipcodebase API
    zipcodes = {
        'San Francisco': ['94101', '94102', '94103', '94104', '94105', '94107', '94108', '94109', '94110', '94111', '94112', '94114', '94115', '94116', '94117', '94118', '94119', '94120', '94121', '94122', '94123', '94124', '94125', '94126', '94127', '94128', '94129', '94130', '94131', '94132', '94133', '94134', '94140', '94141', '94142', '94146', '94147', '94157', '94159', '94164', '94165', '94166', '94167', '94168', '94169', '94170', '94172', '94188'],
        'Oakland': ['94601', '94602', '94603', '94604', '94605', '94606', '94607', '94608', '94609', '94610', '94611', '94612', '94613', '94614', '94615', '94617', '94618', '94619', '94620', '94621', '94623', '94624', '94661', '94662'],
        'Berkeley': ['94701', '94702', '94703', '94704', '94705', '94706', '94707', '94708', '94709', '94710', '94712']
    }
    
    city = st.selectbox("City", ['San Francisco', 'Oakland', 'Berkeley'])
    zipcode = st.selectbox("Zipcode", ['Unsure'] + zipcodes[city])
        
    sex = st.radio("Sex", ["Male", "Female", "Other"])
    lgbtq = st.radio("Do you identify as LGBTQ+ (some shelters serve this community specifically)", ["No", "Yes"])
    domestic_violence = st.radio("Have you experienced domestic violence (some shelters serve these individuals specifically", ["No", "Yes"])
    
    urgency = st.radio("How quickly do you need help?", ("Today", "In the next few days", "In a week or more"))
    duration = st.radio("How long do you need a place to stay?", ("Overnight", "A month or less", "A couple of months", "A year or more"))
    needs = st.text_area("Optional - Needs (tell us what you need and how we can help)")

    phone_number = st.text_input('Optional - Enter your phone number (to text shelter info to you)', '+1')
    consent = st.checkbox('I consent to receiving a one-time message')

    if st.button("Submit"):
        data = {
            "City": city,
            "Zip Code": zipcode,
            "Sex": sex,
            "LGBTQ": lgbtq,
            "Domestic Violence": domestic_violence,
            "Urgency": urgency,
            "Duration": duration,
            "Needs": needs,
            "Phone Number": phone_number,
            "Consent": consent
        }

        with open('data.json', 'w') as f:
            json.dump(data, f)

        st.session_state.form_submitted = True
        st.rerun()
else:
    if not st.session_state.shelters_filtered:
        with open('data.json', 'r') as f:
            data = json.load(f)

        shelters = pd.read_csv("database.csv")
    
        # filter city
        shelters = shelters[(shelters['City'] == data['City'])]
        
        # filter sex
        shelters = shelters[(shelters['Sex'] == data['Sex']) | (shelters['Sex'] == 'All')]
    
        # filter lgbtq
        if data['LGBTQ'] == 'No':
            shelters = shelters[(shelters['LGBTQ'] == "No")]

        # filter domestic violence
        if data['Domestic Violence'] == "No":
            shelters = shelters[(shelters['Domestic Violence'] == "No")]
    
        # keep track of which scores are calculated
        scores = []
        
        # calculate distances between zipcodes
        if data['Zip Code'] != "Unsure":
            geocoding_api_key = os.environ['GoogleAPI']
            
            shelters_coordinates = shelters.apply(lambda row: geocode_address(f"{row['Address']}, {row['City']}, CA {row['Zip Code']}", geocoding_api_key), axis=1).tolist()
            user_coordinates = geocode_address(f"{data['City']}, CA {data['Zip Code']}", geocoding_api_key)
        
            distances = []
            for coordinates in shelters_coordinates:
                 distances.append(haversine(coordinates[0], coordinates[1], user_coordinates[0], user_coordinates[1]))
        
            max_d = max(distances) if (max(distances) != 0) else 1
            shelters['zipcode_score'] = [d / max_d for d in distances]
            scores.append('zipcode_score')
            print('zipcode_score')
    
        # get urgency scores 
        urgency_scores = shelters.apply(lambda row: get_urgency_score(data['Urgency'], row['Urgency']), axis=1).tolist()
        shelters['urgency_score'] = urgency_scores
        scores.append('urgency_score')
    
        # get duration scores
        duration_scores = shelters.apply(lambda row: get_duration_score(data['Duration'], row['Duration']), axis=1).tolist()
        shelters['duration_score'] = duration_scores
        scores.append('duration_score')
    
        # get services scores
        if data['Needs'] != "":     
            OpenAI_API_KEY = os.environ["OPENAI_API_KEY"]
            
            services_scores = shelters.apply(lambda row: call_gpt(data['Needs'], row['Services'], OpenAI_API_KEY), axis=1).tolist()
            services_scores = [s / 10 for s in services_scores]
            
            shelters['services_score'] = services_scores
            scores.append('services_score')

        # get time-based scores
        time_scores = shelters.apply(lambda row: get_time_score(datetime.now(timezone), row), axis=1).tolist()
        
        if data['Urgency'] == "Today": 
            for i in range(len(scores)):
                shelters[f'time_score_{i}'] = time_scores
                scores.append(f'time_score_{i}')
        elif data['Urgency'] == "In the next few days":
            shelters['time_score'] = time_scores
            scores.append('time_score')
        elif data['Urgency'] == "In a week or more":
            pass

        print(scores)
            
        # calcualte cumulative score
        shelters['total_score'] = shelters[scores].sum(axis=1)
        shelters['total_score'] = shelters['total_score'] / len(scores)
    
        shelters = shelters.sort_values(by='total_score', ascending=True)
        shelters = shelters.head(3)

        # convert pandas df into list of dicts
        shelters = shelters.to_dict(orient='records')

        # text messaging
        if len(data['Phone Number']) == 12 and data['Consent']:
            try:
                account_sid = os.environ["SID"]
                auth_token = os.environ["auth_token"]
                client = Client(account_sid, auth_token)
    
                message_body = "Here's some key shelter information from using ShelterSearch today:\n\n"
                for i in range(len(shelters)):
                    phone = str(shelters[i]['Phone'])
                    message_body += f"{shelters[i]['Organization Name']}: {shelters[i]['Program Name']}\n"
                    message_body += f"🕒 Open Hours: {shelters[i]['Open Hours']}\n"
                    message_body += f"📍 Address: {shelters[i]['Address']}\n"
                    message_body += f"📞 Phone Number: ({phone[1:4]}) {phone[4:7]}-{phone[7:]}\n\n"
            
                message = client.messages.create(
                    body = message_body,
                    from_= "+15107212356",
                    to = data['Phone Number']
                ) 
            except: pass

        st.session_state.shelters_filtered = True
        st.session_state.shelters = shelters

    # Display the current shelter information
    shelter = st.session_state.shelters[st.session_state.shelter_index]
    
    st.header(f"{shelter['Organization Name']}: {shelter['Program Name']}")
    st.subheader(f"{shelter['Type']}")
    st.divider()

    st.subheader("Shelter Summary")
    st.write(shelter['Summary'])
    st.divider()

    st.subheader("How to Receive Help")
    st.write(shelter['Application Details'])
    st.markdown(f"- **🕒\tOpen Hours**: {shelter['Open Hours']}")
    st.markdown(f"- **📍\tAddress**: {shelter['Address']}")
    
    phone_number = str(shelter['Phone'])
    formatted_phone_number = f"({phone_number[1:4]}) {phone_number[4:7]}-{phone_number[7:]}"
    phone_link = f"<a href='tel:{phone_number}'>{formatted_phone_number}</a>"
    st.markdown(f"- **📞\tPhone Number**: {phone_link}", unsafe_allow_html=True)
    st.divider()

    with st.expander("More Information"):
        tabs = st.tabs(["Full List of Services", "More About the Program", "More About the Organization", "Webpage Link"])

        with tabs[0]:
            st.write(shelter['Services'])
        
        with tabs[1]:
            st.write(shelter['Program About'])
    
        with tabs[2]:
            st.write(shelter['Organization About'])
    
        with tabs[3]:
            st.write(shelter['Webpage'])
    st.divider()

    # Create map for address
    map = create_map()
    
    key = os.environ['GoogleAPI']
    address = f"{shelter['Address']}, {shelter['City']}, CA"
    lat, long = geocode_address(address, key)
    
    # Fit the map bounds to include all markers
    south_west = [lat - 0.02, long - 0.02]
    north_east = [lat + 0.02, long + 0.02]
    map_bounds = [south_west, north_east]
    map.fit_bounds(map_bounds)
    
    folium.Marker([lat, long], popup=shelter['Address']).add_to(map)
    folium_static(map)
    st.markdown(f" ## [Get Directions](https://www.google.com/maps/dir/?api=1&origin=current+location&destination={lat},{long})")
    st.divider()
            
    # Create two columns
    col1, col2, col3 = st.columns([1,1,1])
    
    # Add buttons to each column
    with col1:
        if st.button("Previous"):
            if st.session_state.shelter_index > 0:
                st.session_state.shelter_index -= 1
                st.rerun()
    
    with col2:
        if st.button("Next"):
            if st.session_state.shelter_index < 2:
                st.session_state.shelter_index += 1
                st.rerun()

    with col3:
        if st.button("Reset"):
            st.session_state.shelter_index = 0
            st.session_state.form_submitted = False
            st.session_state.shelters_filtered = False
            st.rerun()