Sayantan Das commited on
Commit
b553871
·
1 Parent(s): ef2913c

Add application file

Browse files
Files changed (1) hide show
  1. app.py +199 -0
app.py ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import geocoder
3
+ import pandas as pd
4
+ import requests
5
+ import urllib.parse
6
+ from typing import Dict, Any
7
+ from datetime import datetime
8
+ import math
9
+ from typing import Tuple, List, Optional
10
+ import json
11
+
12
+ class CrimeData:
13
+ def __init__(self, incident_type: str, date_time: datetime, location: Tuple[float, float],
14
+ address: str, narrative: str = None):
15
+ self.incident_type = incident_type
16
+ self.date_time = date_time
17
+ self.location = location
18
+ self.address = address
19
+ self.narrative = narrative
20
+
21
+ def haversine_distance(coord1: Tuple[float, float], coord2: Tuple[float, float]) -> float:
22
+ """Calculate the distance between two coordinates in kilometers."""
23
+ R = 6371.0
24
+ lat1, lon1 = math.radians(coord1[0]), math.radians(coord1[1])
25
+ lat2, lon2 = math.radians(coord2[0]), math.radians(coord2[1])
26
+
27
+ dlat = lat2 - lat1
28
+ dlon = lon2 - lon1
29
+
30
+ a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2
31
+ c = 2 * math.asin(math.sqrt(a))
32
+
33
+ return R * c
34
+
35
+ def geocode_address(address: str, city: str = "Kingston", province: str = "ON") -> Optional[Tuple[float, float]]:
36
+ """Convert address to latitude/longitude coordinates."""
37
+ full_address = f"{address}, {city}, {province}, Canada"
38
+ location = geocoder.osm(full_address, headers={
39
+ 'User-Agent': 'CrimeLookupApp/1.0 ([email protected])'
40
+ })
41
+
42
+ if location.ok:
43
+ return (location.lat, location.lng)
44
+
45
+ # Fallback: Direct request to Nominatim API
46
+ import requests
47
+ import urllib.parse
48
+
49
+ url = f"https://nominatim.openstreetmap.org/search"
50
+ params = {
51
+ 'q': full_address,
52
+ 'format': 'jsonv2',
53
+ 'addressdetails': 1,
54
+ 'limit': 1
55
+ }
56
+
57
+ headers = {
58
+ 'User-Agent': 'CrimeLookupApp/1.0 ([email protected])'
59
+ }
60
+
61
+ try:
62
+ response = requests.get(url, params=params, headers=headers)
63
+ response.raise_for_status()
64
+ results = response.json()
65
+
66
+ if results:
67
+ return (float(results[0]['lat']), float(results[0]['lon']))
68
+ except Exception as e:
69
+ print(f"Geocoding error: {e}")
70
+
71
+ return None
72
+
73
+ def load_crime_data() -> List[CrimeData]:
74
+ """Load crime data from the police incidents API."""
75
+ url = "https://ce-portal-service.commandcentral.com/api/v1.0/public/incidents"
76
+
77
+ # Define the request payload with the Kingston area boundaries
78
+ payload = {
79
+ "limit": 2000,
80
+ "offset": 0,
81
+ "geoJson": {
82
+ "type": "Polygon",
83
+ "coordinates": [[
84
+ [-76.5167293, 44.2255476],
85
+ [-76.5167293, 44.2435476],
86
+ [-76.4987293, 44.2435476],
87
+ [-76.4987293, 44.2255476],
88
+ [-76.5167293, 44.2255476]
89
+ ]]
90
+ },
91
+ "projection": False,
92
+ "propertyMap": {
93
+ "pageSize": "2000",
94
+ "zoomLevel": "15",
95
+ "latitude": "44.2345476",
96
+ "longitude": "-76.5077293",
97
+ "relativeDate": "custom",
98
+ "fromDate": "2024-01-11T14:00:00.000Z",
99
+ "toDate": "2025-01-11T13:00:00.000Z",
100
+ "days": "",
101
+ "startHour": "0",
102
+ "endHour": "24",
103
+ "parentIncidentTypeIds": "149,150,148,8,97,104,165,98,100,179,178,180,101,99,103,163,168,166,12,161,14,16,15,160,121,162,164,167,173,169,170,172,171,151",
104
+ "agencyIds": "407,1358,ottawapolice.ca,kpf.ca"
105
+ }
106
+ }
107
+
108
+ headers = {
109
+ 'Content-Type': 'application/json',
110
+ 'Accept': 'application/json',
111
+ 'Origin': 'https://www.cityprotect.com',
112
+ 'Referer': 'https://www.cityprotect.com/'
113
+ }
114
+
115
+ try:
116
+ response = requests.post(url, json=payload, headers=headers)
117
+ response.raise_for_status()
118
+ data = response.json()
119
+
120
+ crimes = []
121
+ if 'result' in data and 'list' in data['result'] and 'incidents' in data['result']['list']:
122
+ for incident in data['result']['list']['incidents']:
123
+ crimes.append(CrimeData(
124
+ incident_type=incident['incidentType'],
125
+ date_time=datetime.fromisoformat(incident['date'].rstrip('Z')),
126
+ location=(incident['location']['coordinates'][1], incident['location']['coordinates'][0]),
127
+ address=incident['blockizedAddress'],
128
+ narrative=incident.get('narrative', '')
129
+ ))
130
+ return crimes
131
+ except Exception as e:
132
+ print(f"Error loading crime data: {e}")
133
+ return []
134
+
135
+ def filter_crimes(center: Tuple[float, float], radius_km: float,
136
+ crimes: List[CrimeData], start_date: datetime, end_date: datetime) -> List[CrimeData]:
137
+ """Filter crimes by distance and date range."""
138
+ return [crime for crime in crimes
139
+ if (haversine_distance(center, crime.location) <= radius_km and
140
+ start_date <= crime.date_time <= end_date)]
141
+
142
+ def format_crime_report(crimes: List[CrimeData]) -> pd.DataFrame:
143
+ """Format crimes into a DataFrame for display."""
144
+ data = []
145
+ for crime in crimes:
146
+ data.append({
147
+ 'Date': crime.date_time.strftime('%Y-%m-%d %H:%M'),
148
+ 'Type': crime.incident_type,
149
+ 'Location': crime.address,
150
+ 'Details': crime.narrative
151
+ })
152
+ return pd.DataFrame(data)
153
+
154
+ def lookup_crimes(address: str, radius: float = 1.0,
155
+ start_date: str = None, end_date: str = None) -> pd.DataFrame:
156
+ """Main function to lookup crimes near an address within a date range."""
157
+ # Validate and parse dates
158
+ try:
159
+ start = datetime.strptime(start_date, '%Y-%m-%d') if start_date else datetime(2023, 1, 1)
160
+ end = datetime.strptime(end_date, '%Y-%m-%d') if end_date else datetime.now()
161
+ except ValueError:
162
+ return pd.DataFrame({'Error': ['Invalid date format. Please use YYYY-MM-DD']})
163
+
164
+ # Geocode the address
165
+ coords = geocode_address(address)
166
+ if not coords:
167
+ return pd.DataFrame({'Error': ['Address not found']})
168
+
169
+ # Load and filter crimes
170
+ all_crimes = load_crime_data()
171
+ filtered_crimes = filter_crimes(coords, radius, all_crimes, start, end)
172
+
173
+ # Format results
174
+ if not filtered_crimes:
175
+ return pd.DataFrame({'Message': [f'No crimes found in specified radius between {start_date} and {end_date}']})
176
+
177
+ return format_crime_report(filtered_crimes)
178
+
179
+ # Create Gradio interface
180
+ iface = gr.Interface(
181
+ fn=lookup_crimes,
182
+ inputs=[
183
+ gr.Textbox(label="Address (e.g., '503 Victoria St, Kingston')"),
184
+ gr.Slider(minimum=0.1, maximum=5.0, value=1.0, label="Radius (km)"),
185
+ gr.Textbox(label="Start Date (YYYY-MM-DD)", value="2024-02-01"),
186
+ gr.Textbox(label="End Date (YYYY-MM-DD)", value=datetime.now().strftime('%Y-%m-%d'))
187
+ ],
188
+ outputs=gr.Dataframe(),
189
+ title="Neighborhood Crime Lookup",
190
+ description="Enter an address and date range to see crimes in the area. Add street name and number only - city is assumed to be Kingston, ON.",
191
+ examples=[
192
+ ["503 Victoria St", 1.0, "2024-01-01", "2024-01-10"],
193
+ ["417 Princess St", 0.5, "2024-06-01", "2024-12-31"]
194
+ ]
195
+ )
196
+
197
+ # Launch the app
198
+ if __name__ == "__main__":
199
+ iface.launch()