File size: 7,332 Bytes
b553871
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
05062ac
 
b553871
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import gradio as gr
import geocoder
import pandas as pd
import requests
import urllib.parse
from typing import Dict, Any
from datetime import datetime
import math
from typing import Tuple, List, Optional
import json

class CrimeData:
    def __init__(self, incident_type: str, date_time: datetime, location: Tuple[float, float], 
                 address: str, narrative: str = None):
        self.incident_type = incident_type
        self.date_time = date_time
        self.location = location
        self.address = address
        self.narrative = narrative

def haversine_distance(coord1: Tuple[float, float], coord2: Tuple[float, float]) -> float:
    """Calculate the distance between two coordinates in kilometers."""
    R = 6371.0
    lat1, lon1 = math.radians(coord1[0]), math.radians(coord1[1])
    lat2, lon2 = math.radians(coord2[0]), math.radians(coord2[1])
    
    dlat = lat2 - lat1
    dlon = lon2 - lon1
    
    a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2
    c = 2 * math.asin(math.sqrt(a))
    
    return R * c

def geocode_address(address: str, city: str = "Kingston", province: str = "ON") -> Optional[Tuple[float, float]]:
    """Convert address to latitude/longitude coordinates."""
    full_address = f"{address}, {city}, {province}, Canada"
    location = geocoder.osm(full_address, headers={
        'User-Agent': 'CrimeLookupApp/1.0 ([email protected])'
    })
    
    if location.ok:
        return (location.lat, location.lng)
    
    # Fallback: Direct request to Nominatim API
    import requests
    import urllib.parse
    
    url = f"https://nominatim.openstreetmap.org/search"
    params = {
        'q': full_address,
        'format': 'jsonv2',
        'addressdetails': 1,
        'limit': 1
    }
    
    headers = {
        'User-Agent': 'CrimeLookupApp/1.0 ([email protected])'
    }
    
    try:
        response = requests.get(url, params=params, headers=headers)
        response.raise_for_status()
        results = response.json()
        
        if results:
            return (float(results[0]['lat']), float(results[0]['lon']))
    except Exception as e:
        print(f"Geocoding error: {e}")
    
    return None

def load_crime_data() -> List[CrimeData]:
    """Load crime data from the police incidents API."""
    url = "https://ce-portal-service.commandcentral.com/api/v1.0/public/incidents"
    
    # Define the request payload with the Kingston area boundaries
    payload = {
        "limit": 2000,
        "offset": 0,
        "geoJson": {
            "type": "Polygon",
            "coordinates": [[
                [-76.5167293, 44.2255476],
                [-76.5167293, 44.2435476],
                [-76.4987293, 44.2435476],
                [-76.4987293, 44.2255476],
                [-76.5167293, 44.2255476]
            ]]
        },
        "projection": False,
        "propertyMap": {
            "pageSize": "2000",
            "zoomLevel": "15",
            "latitude": "44.2345476",
            "longitude": "-76.5077293",
            "relativeDate": "custom",
            "fromDate": "2024-01-11T14:00:00.000Z",
            "toDate": "2025-01-11T13:00:00.000Z",
            "days": "",
            "startHour": "0",
            "endHour": "24",
            "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",
            "agencyIds": "407,1358,ottawapolice.ca,kpf.ca"
        }
    }
    
    headers = {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'Origin': 'https://www.cityprotect.com',
        'Referer': 'https://www.cityprotect.com/'
    }
    
    try:
        response = requests.post(url, json=payload, headers=headers)
        response.raise_for_status()
        data = response.json()
        
        crimes = []
        if 'result' in data and 'list' in data['result'] and 'incidents' in data['result']['list']:
            for incident in data['result']['list']['incidents']:
                crimes.append(CrimeData(
                    incident_type=incident['incidentType'],
                    date_time=datetime.fromisoformat(incident['date'].rstrip('Z')),
                    location=(incident['location']['coordinates'][1], incident['location']['coordinates'][0]),
                    address=incident['blockizedAddress'],
                    narrative=incident.get('narrative', '')
                ))
        return crimes
    except Exception as e:
        print(f"Error loading crime data: {e}")
        return []

def filter_crimes(center: Tuple[float, float], radius_km: float, 
                   crimes: List[CrimeData], start_date: datetime, end_date: datetime) -> List[CrimeData]:
    """Filter crimes by distance and date range."""
    return [crime for crime in crimes 
            if (haversine_distance(center, crime.location) <= radius_km and
                start_date <= crime.date_time <= end_date)]

def format_crime_report(crimes: List[CrimeData]) -> pd.DataFrame:
    """Format crimes into a DataFrame for display."""
    data = []
    for crime in crimes:
        data.append({
            'Date': crime.date_time.strftime('%Y-%m-%d'),
            'Time': crime.date_time.strftime('%H:%M'),
            'Type': crime.incident_type,
            'Location': crime.address,
        })
    return pd.DataFrame(data)

def lookup_crimes(address: str, radius: float = 1.0, 
                start_date: str = None, end_date: str = None) -> pd.DataFrame:
    """Main function to lookup crimes near an address within a date range."""
    # Validate and parse dates
    try:
        start = datetime.strptime(start_date, '%Y-%m-%d') if start_date else datetime(2023, 1, 1)
        end = datetime.strptime(end_date, '%Y-%m-%d') if end_date else datetime.now()
    except ValueError:
        return pd.DataFrame({'Error': ['Invalid date format. Please use YYYY-MM-DD']})
        
    # Geocode the address
    coords = geocode_address(address)
    if not coords:
        return pd.DataFrame({'Error': ['Address not found']})
    
    # Load and filter crimes
    all_crimes = load_crime_data()
    filtered_crimes = filter_crimes(coords, radius, all_crimes, start, end)
    
    # Format results
    if not filtered_crimes:
        return pd.DataFrame({'Message': [f'No crimes found in specified radius between {start_date} and {end_date}']})
    
    return format_crime_report(filtered_crimes)

# Create Gradio interface
iface = gr.Interface(
    fn=lookup_crimes,
    inputs=[
        gr.Textbox(label="Address (e.g., '503 Victoria St, Kingston')"),
        gr.Slider(minimum=0.1, maximum=5.0, value=1.0, label="Radius (km)"),
        gr.Textbox(label="Start Date (YYYY-MM-DD)", value="2024-02-01"),
        gr.Textbox(label="End Date (YYYY-MM-DD)", value=datetime.now().strftime('%Y-%m-%d'))
    ],
    outputs=gr.Dataframe(),
    title="Neighborhood Crime Lookup",
    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.",
    examples=[
        ["503 Victoria St", 1.0, "2024-01-01", "2024-01-10"],
        ["417 Princess St", 0.5, "2024-06-01", "2024-12-31"]
    ]
)

# Launch the app
if __name__ == "__main__":
    iface.launch()