ygk-crime / app.py
ucalyptus's picture
Update app.py
05062ac verified
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()