geocalc-mcp / route_utils.py
Renzo
Add Points of Interest feature and update documentation
e7c5429
import os
import uuid
import tempfile
import staticmaps
from PIL import Image
from geopy.geocoders import Nominatim
from ors_client import get_ors_client, is_ors_configured
TEMP_DIR = tempfile.mkdtemp(prefix="geocalc_")
def generate_route_image_file(coords, start_lat, start_lon, end_lat, end_lon):
"""Generate route image and save to temp file."""
context = staticmaps.Context()
context.set_tile_provider(staticmaps.tile_provider_OSM)
# Create route line
line = staticmaps.Line(
[staticmaps.create_latlng(lat, lng) for lat, lng in coords],
color=staticmaps.BLUE,
width=4
)
context.add_object(line)
# Add start marker (green)
start_marker = staticmaps.Marker(
staticmaps.create_latlng(start_lat, start_lon),
color=staticmaps.GREEN,
size=12
)
context.add_object(start_marker)
# Add end marker (red)
end_marker = staticmaps.Marker(
staticmaps.create_latlng(end_lat, end_lon),
color=staticmaps.RED,
size=12
)
context.add_object(end_marker)
# Generate image and save to temp file
image = context.render_pillow(500, 375)
# Create unique temp file path
filename = f"route_{uuid.uuid4().hex[:8]}.webp"
filepath = os.path.join(TEMP_DIR, filename)
image.save(filepath, format='WEBP', quality=85, optimize=True)
return filepath
def load_image_with_title(image_path, custom_title=None):
"""Load image from path and optionally add custom title."""
image = Image.open(image_path)
if custom_title:
from PIL import ImageDraw, ImageFont
draw = ImageDraw.Draw(image)
# Try to use a nice font, scaled to image size
font_size = max(16, image.width // 25) # Responsive font size
try:
font = ImageFont.truetype("Arial.ttf", font_size)
except:
font = ImageFont.load_default()
# Add title at top center
text_bbox = draw.textbbox((0, 0), custom_title, font=font)
text_width = text_bbox[2] - text_bbox[0]
x = (image.width - text_width) // 2
y = 5 # Reduced margin for smaller images
# Add background rectangle for better readability
padding = 3 # Smaller padding for smaller images
draw.rectangle([x-padding, y-padding, x+text_width+padding, y+text_bbox[3]+padding],
fill="white", outline="black")
draw.text((x, y), custom_title, fill="black", font=font)
return image
def geocode_address(address):
"""Convert address to coordinates."""
geolocator = Nominatim(user_agent="geocalc_mcp_app_hackathon")
location = geolocator.geocode(address)
if location:
return round(location.latitude, 4), round(location.longitude, 4)
return None
def calculate_route_distance_km(route_data):
"""Extract distance in km from route data."""
if "error" in route_data:
return None
meters = route_data['summary']['distance']
return round(meters / 1000, 1)
def calculate_route_time_minutes(route_data):
"""Extract time in minutes from route data."""
if "error" in route_data:
return None
seconds = route_data['summary']['duration']
return int(seconds / 60)
POI_CATEGORIES = {
"accommodation": 108, # hotel
"restaurants": 570, # restaurant
"bars": 561, # bar
"cafes": 564, # café
"healthcare": 208, # pharmacy
"shopping": 518, # supermarket
"attractions": 622, # attraction
"museums": 134, # museum
"transport": 588, # bus_stop
"banks": 192 # bank
}
POI_CATEGORY_LIST = list(POI_CATEGORIES.keys())
def get_poi_data(lat, lon, radius_m=1000, categories=None):
"""Get POI data from OpenRouteService."""
if not is_ors_configured():
return {"error": "ORS API key not configured"}
client_ors = get_ors_client()
# Create circular geometry around the point
import math
# Calculate circle geometry in lat/lon degrees
earth_radius_m = 6371000
lat_rad = math.radians(lat)
lon_rad = math.radians(lon)
# Calculate degree offsets for the radius
lat_offset = radius_m / earth_radius_m
lon_offset = radius_m / (earth_radius_m * math.cos(lat_rad))
# Create a rough circle geometry
circle_points = []
for angle in range(0, 360, 30): # 12 points for circle
angle_rad = math.radians(angle)
point_lat = lat + lat_offset * math.sin(angle_rad)
point_lon = lon + lon_offset * math.cos(angle_rad)
circle_points.append([point_lon, point_lat])
circle_points.append(circle_points[0]) # Close the polygon
geojson_geometry = {
"type": "Polygon",
"coordinates": [circle_points]
}
params = {
"request": "pois",
"geojson": geojson_geometry,
"sortby": "distance"
}
if categories:
category_ids = [POI_CATEGORIES.get(cat) for cat in categories if cat in POI_CATEGORIES]
if category_ids:
# Limit to maximum 5 categories as per API requirement
params["filter_category_ids"] = category_ids[:5]
try:
result = client_ors.places(**params)
return result
except Exception as e:
return {"error": f"POI API request failed: {str(e)}"}