diff --git a/geo_app/schema.py b/geo_app/schema.py index 0b898eb..03a3b43 100644 --- a/geo_app/schema.py +++ b/geo_app/schema.py @@ -292,6 +292,45 @@ class Query(graphene.ObjectType): description="Get route from a specific offer to hub", ) + # New unified endpoints (coordinate-based) + nearest_hubs = graphene.List( + NodeType, + lat=graphene.Float(required=True, description="Latitude"), + lon=graphene.Float(required=True, description="Longitude"), + radius=graphene.Float(default_value=1000, description="Search radius in km"), + product_uuid=graphene.String(description="Filter hubs by product availability"), + limit=graphene.Int(default_value=12, description="Max results"), + description="Find nearest hubs to coordinates (optionally filtered by product)", + ) + + nearest_offers = graphene.List( + OfferNodeType, + lat=graphene.Float(required=True, description="Latitude"), + lon=graphene.Float(required=True, description="Longitude"), + radius=graphene.Float(default_value=500, description="Search radius in km"), + product_uuid=graphene.String(description="Filter by product UUID"), + limit=graphene.Int(default_value=50, description="Max results"), + description="Find nearest offers to coordinates (optionally filtered by product)", + ) + + nearest_suppliers = graphene.List( + SupplierType, + lat=graphene.Float(required=True, description="Latitude"), + lon=graphene.Float(required=True, description="Longitude"), + radius=graphene.Float(default_value=1000, description="Search radius in km"), + product_uuid=graphene.String(description="Filter by product UUID"), + limit=graphene.Int(default_value=12, description="Max results"), + description="Find nearest suppliers to coordinates (optionally filtered by product)", + ) + + route_to_coordinate = graphene.Field( + ProductRouteOptionType, + offer_uuid=graphene.String(required=True, description="Starting offer UUID"), + lat=graphene.Float(required=True, description="Destination latitude"), + lon=graphene.Float(required=True, description="Destination longitude"), + description="Get route from offer to target coordinates (finds nearest hub to coordinate)", + ) + @staticmethod def _build_routes(db, from_uuid, to_uuid, limit): """Shared helper to compute K shortest routes between two nodes.""" @@ -1348,6 +1387,197 @@ class Query(graphene.ObjectType): logger.info("No route found from offer %s to hub %s", offer_uuid, hub_uuid) return None + def resolve_nearest_hubs(self, info, lat, lon, radius=1000, product_uuid=None, limit=12): + """Find nearest hubs to coordinates, optionally filtered by product availability.""" + db = get_db() + + if product_uuid: + # Find hubs that have offers for this product within radius + aql = """ + FOR offer IN nodes + FILTER offer.node_type == 'offer' + FILTER offer.product_uuid == @product_uuid + FILTER offer.latitude != null AND offer.longitude != null + LET dist_to_offer = DISTANCE(offer.latitude, offer.longitude, @lat, @lon) / 1000 + FILTER dist_to_offer <= @radius + + FOR hub IN nodes + FILTER hub.node_type == 'logistics' OR hub.node_type == null + FILTER hub.product_uuid == null + FILTER hub.latitude != null AND hub.longitude != null + LET dist_to_hub = DISTANCE(hub.latitude, hub.longitude, @lat, @lon) / 1000 + FILTER dist_to_hub <= @radius + COLLECT hub_uuid = hub._key INTO hub_group + LET first_hub = FIRST(hub_group)[0].hub + LET hub_dist = DISTANCE(first_hub.latitude, first_hub.longitude, @lat, @lon) / 1000 + SORT hub_dist ASC + LIMIT @limit + RETURN MERGE(first_hub, {_key: hub_uuid, distance_km: hub_dist}) + """ + bind_vars = {'lat': lat, 'lon': lon, 'radius': radius, 'product_uuid': product_uuid, 'limit': limit} + else: + # Simple nearest hubs search + aql = """ + FOR hub IN nodes + FILTER hub.node_type == 'logistics' OR hub.node_type == null + FILTER hub.product_uuid == null + FILTER hub.latitude != null AND hub.longitude != null + LET dist = DISTANCE(hub.latitude, hub.longitude, @lat, @lon) / 1000 + FILTER dist <= @radius + SORT dist ASC + LIMIT @limit + RETURN MERGE(hub, {distance_km: dist}) + """ + bind_vars = {'lat': lat, 'lon': lon, 'radius': radius, 'limit': limit} + + try: + cursor = db.aql.execute(aql, bind_vars=bind_vars) + hubs = [] + for node in cursor: + hubs.append(NodeType( + uuid=node['_key'], + name=node.get('name'), + latitude=node.get('latitude'), + longitude=node.get('longitude'), + country=node.get('country'), + country_code=node.get('country_code'), + synced_at=node.get('synced_at'), + transport_types=node.get('transport_types') or [], + edges=[], + )) + logger.info("Found %d hubs near (%.3f, %.3f) within %d km", len(hubs), lat, lon, radius) + return hubs + except Exception as e: + logger.error("Error finding nearest hubs: %s", e) + return [] + + def resolve_nearest_offers(self, info, lat, lon, radius=500, product_uuid=None, limit=50): + """Find nearest offers to coordinates, optionally filtered by product.""" + db = get_db() + + aql = """ + FOR offer IN nodes + FILTER offer.node_type == 'offer' + FILTER offer.product_uuid != null + FILTER offer.latitude != null AND offer.longitude != null + """ + + if product_uuid: + aql += " FILTER offer.product_uuid == @product_uuid\n" + + aql += """ + LET dist = DISTANCE(offer.latitude, offer.longitude, @lat, @lon) / 1000 + FILTER dist <= @radius + SORT dist ASC + LIMIT @limit + RETURN MERGE(offer, {distance_km: dist}) + """ + + bind_vars = {'lat': lat, 'lon': lon, 'radius': radius, 'limit': limit} + if product_uuid: + bind_vars['product_uuid'] = product_uuid + + try: + cursor = db.aql.execute(aql, bind_vars=bind_vars) + offers = [] + for node in cursor: + offers.append(OfferNodeType( + uuid=node['_key'], + product_uuid=node.get('product_uuid'), + product_name=node.get('product_name'), + supplier_uuid=node.get('supplier_uuid'), + supplier_name=node.get('supplier_name'), + latitude=node.get('latitude'), + longitude=node.get('longitude'), + country=node.get('country'), + country_code=node.get('country_code'), + price_per_unit=node.get('price_per_unit'), + currency=node.get('currency'), + quantity=node.get('quantity'), + unit=node.get('unit'), + )) + logger.info("Found %d offers near (%.3f, %.3f) within %d km", len(offers), lat, lon, radius) + return offers + except Exception as e: + logger.error("Error finding nearest offers: %s", e) + return [] + + def resolve_nearest_suppliers(self, info, lat, lon, radius=1000, product_uuid=None, limit=12): + """Find nearest suppliers to coordinates, optionally filtered by product.""" + db = get_db() + + aql = """ + FOR offer IN nodes + FILTER offer.node_type == 'offer' + FILTER offer.supplier_uuid != null + FILTER offer.latitude != null AND offer.longitude != null + """ + + if product_uuid: + aql += " FILTER offer.product_uuid == @product_uuid\n" + + aql += """ + LET dist = DISTANCE(offer.latitude, offer.longitude, @lat, @lon) / 1000 + FILTER dist <= @radius + COLLECT supplier_uuid = offer.supplier_uuid INTO offers + LET first_offer = FIRST(offers).offer + LET supplier_dist = DISTANCE(first_offer.latitude, first_offer.longitude, @lat, @lon) / 1000 + SORT supplier_dist ASC + LIMIT @limit + RETURN { + uuid: supplier_uuid, + distance_km: supplier_dist + } + """ + + bind_vars = {'lat': lat, 'lon': lon, 'radius': radius, 'limit': limit} + if product_uuid: + bind_vars['product_uuid'] = product_uuid + + try: + cursor = db.aql.execute(aql, bind_vars=bind_vars) + suppliers = [] + for s in cursor: + suppliers.append(SupplierType(uuid=s['uuid'])) + logger.info("Found %d suppliers near (%.3f, %.3f) within %d km", len(suppliers), lat, lon, radius) + return suppliers + except Exception as e: + logger.error("Error finding nearest suppliers: %s", e) + return [] + + def resolve_route_to_coordinate(self, info, offer_uuid, lat, lon): + """Get route from offer to target coordinates (finds nearest hub automatically).""" + db = get_db() + + # Find nearest hub to target coordinates + aql_hub = """ + FOR hub IN nodes + FILTER hub.node_type == 'logistics' OR hub.node_type == null + FILTER hub.product_uuid == null + FILTER hub.latitude != null AND hub.longitude != null + LET dist = DISTANCE(hub.latitude, hub.longitude, @lat, @lon) / 1000 + SORT dist ASC + LIMIT 1 + RETURN hub + """ + + try: + cursor = db.aql.execute(aql_hub, bind_vars={'lat': lat, 'lon': lon}) + hubs = list(cursor) + if not hubs: + logger.info("No hub found near coordinates (%.3f, %.3f)", lat, lon) + return None + + nearest_hub = hubs[0] + hub_uuid = nearest_hub['_key'] + logger.info("Found nearest hub %s to coordinates (%.3f, %.3f)", hub_uuid, lat, lon) + + # Use existing offer_to_hub logic + return self.resolve_offer_to_hub(info, offer_uuid, hub_uuid) + except Exception as e: + logger.error("Error finding route to coordinates: %s", e) + return None + schema = graphene.Schema(query=Query)