"""Apple Maps tools.""" import subprocess import urllib.parse from helpers import run_applescript, run_jxa, safe_applescript_string def search_locations(query: str) -> list[dict]: """Search for locations using Maps.""" safe_query = safe_applescript_string(query) # Use JXA with MapKit-like search via Maps app script = f''' var Maps = Application("Maps"); Maps.activate(); // Use URL scheme to search var app = Application.currentApplication(); app.includeStandardAdditions = true; app.openLocation("maps://?q=" + encodeURIComponent("{safe_query}")); // Return the query - Maps will show results in the app "{safe_query}"; ''' # Maps doesn't expose search results via AppleScript, so we open the search # and also try to geocode using CoreLocation via JXA geocode_script = f''' ObjC.import("CoreLocation"); ObjC.import("Foundation"); var geocoder = $.CLGeocoder.alloc.init; var query = "{safe_query}"; var results = []; var done = false; geocoder.geocodeAddressStringCompletionHandler(query, function(placemarks, error) {{ if (placemarks && placemarks.count > 0) {{ for (var i = 0; i < Math.min(placemarks.count, 5); i++) {{ var pm = placemarks.objectAtIndex(i); var loc = pm.location; var name = pm.name ? pm.name.js : query; var locality = pm.locality ? pm.locality.js : ""; var admin = pm.administrativeArea ? pm.administrativeArea.js : ""; var country = pm.country ? pm.country.js : ""; var postal = pm.postalCode ? pm.postalCode.js : ""; results.push({{ name: name, latitude: loc.coordinate.latitude, longitude: loc.coordinate.longitude, locality: locality, state: admin, country: country, postal_code: postal }}); }} }} done = true; }}); // Wait for geocoding (up to 10 seconds) var startTime = new Date().getTime(); while (!done && (new Date().getTime() - startTime) < 10000) {{ $.NSRunLoop.currentRunLoop.runUntilDate($.NSDate.dateWithTimeIntervalSinceNow(0.1)); }} JSON.stringify(results); ''' try: raw = run_jxa(geocode_script, timeout=15) import json locations = json.loads(raw) return locations except Exception: # Fallback: just open in Maps encoded = urllib.parse.quote(query) subprocess.run(["open", f"maps://?q={encoded}"], capture_output=True) return [{"query": query, "opened_in_maps": True, "note": "Search opened in Maps app. CoreLocation geocoding unavailable."}] def get_directions(from_address: str | None = None, to_address: str = "", mode: str = "driving") -> dict: """Get directions between two locations. Args: from_address: Starting address. None = current location. to_address: Destination address. mode: Travel mode - "driving", "walking", "transit". """ mode_map = {"driving": "d", "walking": "w", "transit": "r"} mode_char = mode_map.get(mode, "d") params = {"daddr": to_address, "dirflg": mode_char} if from_address: params["saddr"] = from_address url = "maps://?" + urllib.parse.urlencode(params) subprocess.run(["open", url], capture_output=True) return { "from": from_address or "Current Location", "to": to_address, "mode": mode, "opened_in_maps": True, } def open_location(address: str | None = None, latitude: float | None = None, longitude: float | None = None, label: str | None = None) -> dict: """Open a location in Apple Maps. Provide either an address string or lat/lon coordinates. """ if latitude is not None and longitude is not None: params = {"ll": f"{latitude},{longitude}"} if label: params["q"] = label url = "maps://?" + urllib.parse.urlencode(params) elif address: params = {"address": address} if label: params["q"] = label url = "maps://?" + urllib.parse.urlencode(params) else: raise RuntimeError("Provide either address or latitude/longitude") subprocess.run(["open", url], capture_output=True) return { "address": address, "latitude": latitude, "longitude": longitude, "label": label, "opened_in_maps": True, } def drop_pin(latitude: float, longitude: float, label: str = "Pin") -> dict: """Drop a pin at specific coordinates in Maps.""" params = {"ll": f"{latitude},{longitude}", "q": label} url = "maps://?" + urllib.parse.urlencode(params) subprocess.run(["open", url], capture_output=True) return { "latitude": latitude, "longitude": longitude, "label": label, "dropped_pin": True, }