refactor(geo): Clean up queries - rename offers_to_hub to offers_by_hub, add offer_to_hub
All checks were successful
Build Docker Image / build (push) Successful in 1m24s
All checks were successful
Build Docker Image / build (push) Successful in 1m24s
- Remove find_routes, find_product_routes, delivery_to_hub queries - Rename offers_to_hub → offers_by_hub with proper phase-based routing (auto → rail* → auto) - Add offer_to_hub query for single offer to hub connection - Both new queries use Dijkstra-like search with transport phases
This commit is contained in:
@@ -182,22 +182,6 @@ class Query(graphene.ObjectType):
|
|||||||
description="Get rail route between two points via OpenRailRouting",
|
description="Get rail route between two points via OpenRailRouting",
|
||||||
)
|
)
|
||||||
|
|
||||||
find_routes = graphene.List(
|
|
||||||
RoutePathType,
|
|
||||||
from_uuid=graphene.String(required=True),
|
|
||||||
to_uuid=graphene.String(required=True),
|
|
||||||
limit=graphene.Int(default_value=3),
|
|
||||||
description="Find K shortest routes through graph between two nodes",
|
|
||||||
)
|
|
||||||
|
|
||||||
find_product_routes = graphene.List(
|
|
||||||
ProductRouteOptionType,
|
|
||||||
product_uuid=graphene.String(required=True),
|
|
||||||
to_uuid=graphene.String(required=True),
|
|
||||||
limit_sources=graphene.Int(default_value=3),
|
|
||||||
limit_routes=graphene.Int(default_value=3),
|
|
||||||
description="Find routes from product offer nodes to destination",
|
|
||||||
)
|
|
||||||
|
|
||||||
clustered_nodes = graphene.List(
|
clustered_nodes = graphene.List(
|
||||||
ClusterPointType,
|
ClusterPointType,
|
||||||
@@ -254,19 +238,19 @@ class Query(graphene.ObjectType):
|
|||||||
description="Get products available near a hub",
|
description="Get products available near a hub",
|
||||||
)
|
)
|
||||||
|
|
||||||
offers_to_hub = graphene.List(
|
offers_by_hub = graphene.List(
|
||||||
ProductRouteOptionType,
|
ProductRouteOptionType,
|
||||||
hub_uuid=graphene.String(required=True),
|
hub_uuid=graphene.String(required=True),
|
||||||
product_uuid=graphene.String(required=True),
|
product_uuid=graphene.String(required=True),
|
||||||
limit=graphene.Int(default_value=10),
|
limit=graphene.Int(default_value=10),
|
||||||
description="Get offers for a product with routes to hub",
|
description="Get offers for a product with routes to hub (auto → rail* → auto)",
|
||||||
)
|
)
|
||||||
|
|
||||||
delivery_to_hub = graphene.Field(
|
offer_to_hub = graphene.Field(
|
||||||
ProductRouteOptionType,
|
ProductRouteOptionType,
|
||||||
offer_uuid=graphene.String(required=True),
|
offer_uuid=graphene.String(required=True),
|
||||||
hub_uuid=graphene.String(required=True),
|
hub_uuid=graphene.String(required=True),
|
||||||
description="Get delivery route from offer to hub",
|
description="Get route from a specific offer to hub",
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -657,197 +641,6 @@ class Query(graphene.ObjectType):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def resolve_find_routes(self, info, from_uuid, to_uuid, limit=3):
|
|
||||||
"""Find K shortest routes through graph using ArangoDB K_SHORTEST_PATHS."""
|
|
||||||
db = get_db()
|
|
||||||
ensure_graph()
|
|
||||||
return Query._build_routes(db, from_uuid, to_uuid, limit)
|
|
||||||
|
|
||||||
def resolve_find_product_routes(self, info, product_uuid, to_uuid, limit_sources=3, limit_routes=3):
|
|
||||||
"""
|
|
||||||
Найти до N ближайших офферов и вернуть по одному маршруту:
|
|
||||||
авто -> (rail сколько угодно) -> авто. Поиск идёт от точки назначения наружу.
|
|
||||||
"""
|
|
||||||
db = get_db()
|
|
||||||
ensure_graph() # graph exists, но используем ручной обход
|
|
||||||
|
|
||||||
# Load destination node for distance sorting
|
|
||||||
nodes_col = db.collection('nodes')
|
|
||||||
dest = nodes_col.get(to_uuid)
|
|
||||||
if not dest:
|
|
||||||
logger.info("Destination node %s not found", to_uuid)
|
|
||||||
return []
|
|
||||||
|
|
||||||
dest_lat = dest.get('latitude')
|
|
||||||
dest_lon = dest.get('longitude')
|
|
||||||
if dest_lat is None or dest_lon is None:
|
|
||||||
logger.info("Destination node %s missing coordinates", to_uuid)
|
|
||||||
return []
|
|
||||||
|
|
||||||
max_sources = limit_sources or 5
|
|
||||||
max_routes = 1 # всегда один маршрут на оффер
|
|
||||||
|
|
||||||
# Helpers
|
|
||||||
def allowed_next_phase(current_phase, transport_type):
|
|
||||||
"""
|
|
||||||
Phases — расширение радиуса поиска, ЖД не обязателен:
|
|
||||||
- end_auto: можно 1 авто, rail, или сразу offer
|
|
||||||
- end_auto_done: авто использовано — rail или offer
|
|
||||||
- rail: любое кол-во rail, потом 1 авто или offer
|
|
||||||
- start_auto_done: авто использовано — только offer
|
|
||||||
|
|
||||||
Offer можно найти на любом этапе!
|
|
||||||
"""
|
|
||||||
if current_phase == 'end_auto':
|
|
||||||
if transport_type == 'offer':
|
|
||||||
return 'offer' # нашли сразу рядом
|
|
||||||
if transport_type == 'auto':
|
|
||||||
return 'end_auto_done'
|
|
||||||
if transport_type == 'rail':
|
|
||||||
return 'rail'
|
|
||||||
return None
|
|
||||||
if current_phase == 'end_auto_done':
|
|
||||||
if transport_type == 'offer':
|
|
||||||
return 'offer' # нашли после 1 авто
|
|
||||||
if transport_type == 'rail':
|
|
||||||
return 'rail'
|
|
||||||
return None
|
|
||||||
if current_phase == 'rail':
|
|
||||||
if transport_type == 'offer':
|
|
||||||
return 'offer' # нашли на ЖД станции
|
|
||||||
if transport_type == 'rail':
|
|
||||||
return 'rail'
|
|
||||||
if transport_type == 'auto':
|
|
||||||
return 'start_auto_done'
|
|
||||||
return None
|
|
||||||
if current_phase == 'start_auto_done':
|
|
||||||
if transport_type == 'offer':
|
|
||||||
return 'offer'
|
|
||||||
return None
|
|
||||||
return None
|
|
||||||
|
|
||||||
def fetch_neighbors(node_key, phase):
|
|
||||||
"""Получить соседей с учётом допустимых типов транспорта."""
|
|
||||||
# offer доступен на всех фазах — ищем ближайший
|
|
||||||
if phase == 'end_auto':
|
|
||||||
types = ['auto', 'rail', 'offer']
|
|
||||||
elif phase == 'end_auto_done':
|
|
||||||
types = ['rail', 'offer']
|
|
||||||
elif phase == 'rail':
|
|
||||||
types = ['rail', 'auto', 'offer']
|
|
||||||
elif phase == 'start_auto_done':
|
|
||||||
types = ['offer']
|
|
||||||
else:
|
|
||||||
types = ['offer']
|
|
||||||
|
|
||||||
aql = """
|
|
||||||
FOR edge IN edges
|
|
||||||
FILTER edge.transport_type IN @types
|
|
||||||
FILTER edge._from == @node_id OR edge._to == @node_id
|
|
||||||
LET neighbor_id = edge._from == @node_id ? edge._to : edge._from
|
|
||||||
LET neighbor = DOCUMENT(neighbor_id)
|
|
||||||
FILTER neighbor != null
|
|
||||||
RETURN {
|
|
||||||
neighbor_key: neighbor._key,
|
|
||||||
neighbor_doc: neighbor,
|
|
||||||
from_id: edge._from,
|
|
||||||
to_id: edge._to,
|
|
||||||
transport_type: edge.transport_type,
|
|
||||||
distance_km: edge.distance_km,
|
|
||||||
travel_time_seconds: edge.travel_time_seconds
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
cursor = db.aql.execute(
|
|
||||||
aql,
|
|
||||||
bind_vars={
|
|
||||||
'node_id': f"nodes/{node_key}",
|
|
||||||
'types': types,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return list(cursor)
|
|
||||||
|
|
||||||
# Priority queue: (cost, seq, node_key, phase)
|
|
||||||
queue = []
|
|
||||||
counter = 0
|
|
||||||
heapq.heappush(queue, (0, counter, to_uuid, 'end_auto'))
|
|
||||||
|
|
||||||
visited = {} # (node, phase) -> best_cost
|
|
||||||
predecessors = {} # (node, phase) -> (prev_node, prev_phase, edge_info)
|
|
||||||
node_docs = {to_uuid: dest}
|
|
||||||
|
|
||||||
found_routes = []
|
|
||||||
expansions = 0
|
|
||||||
|
|
||||||
while queue and len(found_routes) < max_sources and expansions < Query.MAX_EXPANSIONS:
|
|
||||||
cost, _, node_key, phase = heapq.heappop(queue)
|
|
||||||
|
|
||||||
if (node_key, phase) in visited and cost > visited[(node_key, phase)]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Если нашли оффер нужного товара в допустимой фазе, фиксируем маршрут
|
|
||||||
node_doc = node_docs.get(node_key)
|
|
||||||
if node_doc and node_doc.get('product_uuid') == product_uuid:
|
|
||||||
path_edges = []
|
|
||||||
state = (node_key, phase)
|
|
||||||
current_key = node_key
|
|
||||||
while state in predecessors:
|
|
||||||
prev_state, edge_info = predecessors[state]
|
|
||||||
prev_key = prev_state[0]
|
|
||||||
path_edges.append((current_key, prev_key, edge_info)) # from source toward dest
|
|
||||||
state = prev_state
|
|
||||||
current_key = prev_key
|
|
||||||
|
|
||||||
route = _build_route_from_edges(path_edges, node_docs)
|
|
||||||
distance_km = None
|
|
||||||
src_lat = node_doc.get('latitude')
|
|
||||||
src_lon = node_doc.get('longitude')
|
|
||||||
if src_lat is not None and src_lon is not None:
|
|
||||||
distance_km = _distance_km(src_lat, src_lon, dest_lat, dest_lon)
|
|
||||||
|
|
||||||
found_routes.append(ProductRouteOptionType(
|
|
||||||
source_uuid=node_key,
|
|
||||||
source_name=node_doc.get('name'),
|
|
||||||
source_lat=node_doc.get('latitude'),
|
|
||||||
source_lon=node_doc.get('longitude'),
|
|
||||||
distance_km=distance_km,
|
|
||||||
routes=[route] if route else [],
|
|
||||||
))
|
|
||||||
# продолжаем искать остальных
|
|
||||||
continue
|
|
||||||
|
|
||||||
neighbors = fetch_neighbors(node_key, phase)
|
|
||||||
expansions += 1
|
|
||||||
|
|
||||||
for neighbor in neighbors:
|
|
||||||
transport_type = neighbor.get('transport_type')
|
|
||||||
next_phase = allowed_next_phase(phase, transport_type)
|
|
||||||
if next_phase is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
travel_time = neighbor.get('travel_time_seconds')
|
|
||||||
distance_km = neighbor.get('distance_km')
|
|
||||||
|
|
||||||
neighbor_key = neighbor.get('neighbor_key')
|
|
||||||
node_docs[neighbor_key] = neighbor.get('neighbor_doc')
|
|
||||||
|
|
||||||
step_cost = travel_time if travel_time is not None else (distance_km or 0)
|
|
||||||
new_cost = cost + step_cost
|
|
||||||
|
|
||||||
state_key = (neighbor_key, next_phase)
|
|
||||||
if state_key in visited and new_cost >= visited[state_key]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
visited[state_key] = new_cost
|
|
||||||
counter += 1
|
|
||||||
heapq.heappush(queue, (new_cost, counter, neighbor_key, next_phase))
|
|
||||||
predecessors[state_key] = ((node_key, phase), neighbor)
|
|
||||||
|
|
||||||
if not found_routes:
|
|
||||||
logger.info("No product routes found for %s -> %s", product_uuid, to_uuid)
|
|
||||||
return []
|
|
||||||
|
|
||||||
return found_routes
|
|
||||||
|
|
||||||
def resolve_clustered_nodes(self, info, west, south, east, north, zoom, transport_type=None):
|
def resolve_clustered_nodes(self, info, west, south, east, north, zoom, transport_type=None):
|
||||||
"""Get clustered nodes for map display using server-side SuperCluster."""
|
"""Get clustered nodes for map display using server-side SuperCluster."""
|
||||||
db = get_db()
|
db = get_db()
|
||||||
@@ -1074,12 +867,17 @@ class Query(graphene.ObjectType):
|
|||||||
logger.error("Error getting products near hub: %s", e)
|
logger.error("Error getting products near hub: %s", e)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def resolve_offers_to_hub(self, info, hub_uuid, product_uuid, limit=10):
|
def resolve_offers_by_hub(self, info, hub_uuid, product_uuid, limit=10):
|
||||||
"""Get offers for a product with routes to hub using DISTANCE()."""
|
"""
|
||||||
|
Get offers for a product with routes to hub.
|
||||||
|
Uses phase-based routing: auto → rail* → auto
|
||||||
|
Search goes from hub outward to find offers.
|
||||||
|
"""
|
||||||
db = get_db()
|
db = get_db()
|
||||||
|
ensure_graph()
|
||||||
nodes_col = db.collection('nodes')
|
nodes_col = db.collection('nodes')
|
||||||
|
|
||||||
# Get hub coordinates
|
# Get hub
|
||||||
hub = nodes_col.get(hub_uuid)
|
hub = nodes_col.get(hub_uuid)
|
||||||
if not hub:
|
if not hub:
|
||||||
logger.info("Hub %s not found", hub_uuid)
|
logger.info("Hub %s not found", hub_uuid)
|
||||||
@@ -1091,65 +889,327 @@ class Query(graphene.ObjectType):
|
|||||||
logger.info("Hub %s missing coordinates", hub_uuid)
|
logger.info("Hub %s missing coordinates", hub_uuid)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Find offers for this product sorted by distance to hub
|
# Phase-based routing: auto → rail* → auto
|
||||||
aql = """
|
def allowed_next_phase(current_phase, transport_type):
|
||||||
FOR node IN nodes
|
"""
|
||||||
FILTER node.node_type == 'offer'
|
Phases — расширение радиуса поиска, ЖД не обязателен:
|
||||||
FILTER node.product_uuid == @product_uuid
|
- end_auto: можно 1 авто, rail, или сразу offer
|
||||||
FILTER node.latitude != null AND node.longitude != null
|
- end_auto_done: авто использовано — rail или offer
|
||||||
LET dist = DISTANCE(node.latitude, node.longitude, @hub_lat, @hub_lon) / 1000
|
- rail: любое кол-во rail, потом 1 авто или offer
|
||||||
SORT dist ASC
|
- start_auto_done: авто использовано — только offer
|
||||||
LIMIT @limit
|
"""
|
||||||
RETURN MERGE(node, {distance_km: dist})
|
if current_phase == 'end_auto':
|
||||||
"""
|
if transport_type == 'offer':
|
||||||
try:
|
return 'offer'
|
||||||
cursor = db.aql.execute(aql, bind_vars={
|
if transport_type == 'auto':
|
||||||
'product_uuid': product_uuid,
|
return 'end_auto_done'
|
||||||
'hub_lat': hub_lat,
|
if transport_type == 'rail':
|
||||||
'hub_lon': hub_lon,
|
return 'rail'
|
||||||
'limit': limit
|
return None
|
||||||
})
|
if current_phase == 'end_auto_done':
|
||||||
offers = list(cursor)
|
if transport_type == 'offer':
|
||||||
logger.info("Found %d offers for product %s near hub %s", len(offers), product_uuid, hub_uuid)
|
return 'offer'
|
||||||
|
if transport_type == 'rail':
|
||||||
|
return 'rail'
|
||||||
|
return None
|
||||||
|
if current_phase == 'rail':
|
||||||
|
if transport_type == 'offer':
|
||||||
|
return 'offer'
|
||||||
|
if transport_type == 'rail':
|
||||||
|
return 'rail'
|
||||||
|
if transport_type == 'auto':
|
||||||
|
return 'start_auto_done'
|
||||||
|
return None
|
||||||
|
if current_phase == 'start_auto_done':
|
||||||
|
if transport_type == 'offer':
|
||||||
|
return 'offer'
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
results = []
|
def fetch_neighbors(node_key, phase):
|
||||||
for offer in offers:
|
"""Get neighbors based on allowed transport types for phase."""
|
||||||
# Build route for each offer
|
if phase == 'end_auto':
|
||||||
routes = Query._build_routes(db, offer['_key'], hub_uuid, limit=1)
|
types = ['auto', 'rail', 'offer']
|
||||||
results.append(ProductRouteOptionType(
|
elif phase == 'end_auto_done':
|
||||||
source_uuid=offer['_key'],
|
types = ['rail', 'offer']
|
||||||
source_name=offer.get('name'),
|
elif phase == 'rail':
|
||||||
source_lat=offer.get('latitude'),
|
types = ['rail', 'auto', 'offer']
|
||||||
source_lon=offer.get('longitude'),
|
elif phase == 'start_auto_done':
|
||||||
distance_km=offer.get('distance_km'),
|
types = ['offer']
|
||||||
routes=routes,
|
else:
|
||||||
|
types = ['offer']
|
||||||
|
|
||||||
|
aql = """
|
||||||
|
FOR edge IN edges
|
||||||
|
FILTER edge.transport_type IN @types
|
||||||
|
FILTER edge._from == @node_id OR edge._to == @node_id
|
||||||
|
LET neighbor_id = edge._from == @node_id ? edge._to : edge._from
|
||||||
|
LET neighbor = DOCUMENT(neighbor_id)
|
||||||
|
FILTER neighbor != null
|
||||||
|
RETURN {
|
||||||
|
neighbor_key: neighbor._key,
|
||||||
|
neighbor_doc: neighbor,
|
||||||
|
from_id: edge._from,
|
||||||
|
to_id: edge._to,
|
||||||
|
transport_type: edge.transport_type,
|
||||||
|
distance_km: edge.distance_km,
|
||||||
|
travel_time_seconds: edge.travel_time_seconds
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
cursor = db.aql.execute(
|
||||||
|
aql,
|
||||||
|
bind_vars={
|
||||||
|
'node_id': f"nodes/{node_key}",
|
||||||
|
'types': types,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return list(cursor)
|
||||||
|
|
||||||
|
# Priority queue: (cost, seq, node_key, phase)
|
||||||
|
queue = []
|
||||||
|
counter = 0
|
||||||
|
heapq.heappush(queue, (0, counter, hub_uuid, 'end_auto'))
|
||||||
|
|
||||||
|
visited = {}
|
||||||
|
predecessors = {}
|
||||||
|
node_docs = {hub_uuid: hub}
|
||||||
|
|
||||||
|
found_routes = []
|
||||||
|
expansions = 0
|
||||||
|
|
||||||
|
while queue and len(found_routes) < limit and expansions < Query.MAX_EXPANSIONS:
|
||||||
|
cost, _, node_key, phase = heapq.heappop(queue)
|
||||||
|
|
||||||
|
if (node_key, phase) in visited and cost > visited[(node_key, phase)]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Found an offer for the product
|
||||||
|
node_doc = node_docs.get(node_key)
|
||||||
|
if node_doc and node_doc.get('product_uuid') == product_uuid:
|
||||||
|
path_edges = []
|
||||||
|
state = (node_key, phase)
|
||||||
|
current_key = node_key
|
||||||
|
while state in predecessors:
|
||||||
|
prev_state, edge_info = predecessors[state]
|
||||||
|
prev_key = prev_state[0]
|
||||||
|
path_edges.append((current_key, prev_key, edge_info))
|
||||||
|
state = prev_state
|
||||||
|
current_key = prev_key
|
||||||
|
|
||||||
|
route = _build_route_from_edges(path_edges, node_docs)
|
||||||
|
distance_km = None
|
||||||
|
src_lat = node_doc.get('latitude')
|
||||||
|
src_lon = node_doc.get('longitude')
|
||||||
|
if src_lat is not None and src_lon is not None:
|
||||||
|
distance_km = _distance_km(src_lat, src_lon, hub_lat, hub_lon)
|
||||||
|
|
||||||
|
found_routes.append(ProductRouteOptionType(
|
||||||
|
source_uuid=node_key,
|
||||||
|
source_name=node_doc.get('name') or node_doc.get('product_name'),
|
||||||
|
source_lat=node_doc.get('latitude'),
|
||||||
|
source_lon=node_doc.get('longitude'),
|
||||||
|
distance_km=distance_km,
|
||||||
|
routes=[route] if route else [],
|
||||||
))
|
))
|
||||||
return results
|
continue
|
||||||
except Exception as e:
|
|
||||||
logger.error("Error getting offers to hub: %s", e)
|
neighbors = fetch_neighbors(node_key, phase)
|
||||||
|
expansions += 1
|
||||||
|
|
||||||
|
for neighbor in neighbors:
|
||||||
|
transport_type = neighbor.get('transport_type')
|
||||||
|
next_phase = allowed_next_phase(phase, transport_type)
|
||||||
|
if next_phase is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
travel_time = neighbor.get('travel_time_seconds')
|
||||||
|
distance_km = neighbor.get('distance_km')
|
||||||
|
|
||||||
|
neighbor_key = neighbor.get('neighbor_key')
|
||||||
|
node_docs[neighbor_key] = neighbor.get('neighbor_doc')
|
||||||
|
|
||||||
|
step_cost = travel_time if travel_time is not None else (distance_km or 0)
|
||||||
|
new_cost = cost + step_cost
|
||||||
|
|
||||||
|
state_key = (neighbor_key, next_phase)
|
||||||
|
if state_key in visited and new_cost >= visited[state_key]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
visited[state_key] = new_cost
|
||||||
|
counter += 1
|
||||||
|
heapq.heappush(queue, (new_cost, counter, neighbor_key, next_phase))
|
||||||
|
predecessors[state_key] = ((node_key, phase), neighbor)
|
||||||
|
|
||||||
|
if not found_routes:
|
||||||
|
logger.info("No offers found for product %s near hub %s", product_uuid, hub_uuid)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def resolve_delivery_to_hub(self, info, offer_uuid, hub_uuid):
|
logger.info("Found %d offers for product %s near hub %s", len(found_routes), product_uuid, hub_uuid)
|
||||||
"""Get delivery route from offer to hub."""
|
return found_routes
|
||||||
|
|
||||||
|
def resolve_offer_to_hub(self, info, offer_uuid, hub_uuid):
|
||||||
|
"""
|
||||||
|
Get route from a specific offer to hub.
|
||||||
|
Uses phase-based routing: auto → rail* → auto
|
||||||
|
"""
|
||||||
db = get_db()
|
db = get_db()
|
||||||
|
ensure_graph()
|
||||||
nodes_col = db.collection('nodes')
|
nodes_col = db.collection('nodes')
|
||||||
|
|
||||||
offer = nodes_col.get(offer_uuid)
|
offer = nodes_col.get(offer_uuid)
|
||||||
if not offer:
|
if not offer:
|
||||||
logger.info("Offer %s not found", offer_uuid)
|
logger.info("Offer %s not found", offer_uuid)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
routes = Query._build_routes(db, offer_uuid, hub_uuid, limit=1)
|
hub = nodes_col.get(hub_uuid)
|
||||||
if not routes:
|
if not hub:
|
||||||
|
logger.info("Hub %s not found", hub_uuid)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return ProductRouteOptionType(
|
hub_lat = hub.get('latitude')
|
||||||
source_uuid=offer_uuid,
|
hub_lon = hub.get('longitude')
|
||||||
source_name=offer.get('name'),
|
offer_lat = offer.get('latitude')
|
||||||
source_lat=offer.get('latitude'),
|
offer_lon = offer.get('longitude')
|
||||||
source_lon=offer.get('longitude'),
|
|
||||||
distance_km=routes[0].total_distance_km if routes else None,
|
# Phase-based routing from hub to offer
|
||||||
routes=routes,
|
def allowed_next_phase(current_phase, transport_type):
|
||||||
)
|
if current_phase == 'end_auto':
|
||||||
|
if transport_type == 'offer':
|
||||||
|
return 'offer'
|
||||||
|
if transport_type == 'auto':
|
||||||
|
return 'end_auto_done'
|
||||||
|
if transport_type == 'rail':
|
||||||
|
return 'rail'
|
||||||
|
return None
|
||||||
|
if current_phase == 'end_auto_done':
|
||||||
|
if transport_type == 'offer':
|
||||||
|
return 'offer'
|
||||||
|
if transport_type == 'rail':
|
||||||
|
return 'rail'
|
||||||
|
return None
|
||||||
|
if current_phase == 'rail':
|
||||||
|
if transport_type == 'offer':
|
||||||
|
return 'offer'
|
||||||
|
if transport_type == 'rail':
|
||||||
|
return 'rail'
|
||||||
|
if transport_type == 'auto':
|
||||||
|
return 'start_auto_done'
|
||||||
|
return None
|
||||||
|
if current_phase == 'start_auto_done':
|
||||||
|
if transport_type == 'offer':
|
||||||
|
return 'offer'
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
def fetch_neighbors(node_key, phase):
|
||||||
|
if phase == 'end_auto':
|
||||||
|
types = ['auto', 'rail', 'offer']
|
||||||
|
elif phase == 'end_auto_done':
|
||||||
|
types = ['rail', 'offer']
|
||||||
|
elif phase == 'rail':
|
||||||
|
types = ['rail', 'auto', 'offer']
|
||||||
|
elif phase == 'start_auto_done':
|
||||||
|
types = ['offer']
|
||||||
|
else:
|
||||||
|
types = ['offer']
|
||||||
|
|
||||||
|
aql = """
|
||||||
|
FOR edge IN edges
|
||||||
|
FILTER edge.transport_type IN @types
|
||||||
|
FILTER edge._from == @node_id OR edge._to == @node_id
|
||||||
|
LET neighbor_id = edge._from == @node_id ? edge._to : edge._from
|
||||||
|
LET neighbor = DOCUMENT(neighbor_id)
|
||||||
|
FILTER neighbor != null
|
||||||
|
RETURN {
|
||||||
|
neighbor_key: neighbor._key,
|
||||||
|
neighbor_doc: neighbor,
|
||||||
|
from_id: edge._from,
|
||||||
|
to_id: edge._to,
|
||||||
|
transport_type: edge.transport_type,
|
||||||
|
distance_km: edge.distance_km,
|
||||||
|
travel_time_seconds: edge.travel_time_seconds
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
cursor = db.aql.execute(
|
||||||
|
aql,
|
||||||
|
bind_vars={
|
||||||
|
'node_id': f"nodes/{node_key}",
|
||||||
|
'types': types,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return list(cursor)
|
||||||
|
|
||||||
|
queue = []
|
||||||
|
counter = 0
|
||||||
|
heapq.heappush(queue, (0, counter, hub_uuid, 'end_auto'))
|
||||||
|
|
||||||
|
visited = {}
|
||||||
|
predecessors = {}
|
||||||
|
node_docs = {hub_uuid: hub, offer_uuid: offer}
|
||||||
|
|
||||||
|
expansions = 0
|
||||||
|
|
||||||
|
while queue and expansions < Query.MAX_EXPANSIONS:
|
||||||
|
cost, _, node_key, phase = heapq.heappop(queue)
|
||||||
|
|
||||||
|
if (node_key, phase) in visited and cost > visited[(node_key, phase)]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Found the specific offer
|
||||||
|
if node_key == offer_uuid:
|
||||||
|
path_edges = []
|
||||||
|
state = (node_key, phase)
|
||||||
|
current_key = node_key
|
||||||
|
while state in predecessors:
|
||||||
|
prev_state, edge_info = predecessors[state]
|
||||||
|
prev_key = prev_state[0]
|
||||||
|
path_edges.append((current_key, prev_key, edge_info))
|
||||||
|
state = prev_state
|
||||||
|
current_key = prev_key
|
||||||
|
|
||||||
|
route = _build_route_from_edges(path_edges, node_docs)
|
||||||
|
distance_km = None
|
||||||
|
if offer_lat is not None and offer_lon is not None and hub_lat is not None and hub_lon is not None:
|
||||||
|
distance_km = _distance_km(offer_lat, offer_lon, hub_lat, hub_lon)
|
||||||
|
|
||||||
|
return ProductRouteOptionType(
|
||||||
|
source_uuid=offer_uuid,
|
||||||
|
source_name=offer.get('name') or offer.get('product_name'),
|
||||||
|
source_lat=offer_lat,
|
||||||
|
source_lon=offer_lon,
|
||||||
|
distance_km=distance_km,
|
||||||
|
routes=[route] if route else [],
|
||||||
|
)
|
||||||
|
|
||||||
|
neighbors = fetch_neighbors(node_key, phase)
|
||||||
|
expansions += 1
|
||||||
|
|
||||||
|
for neighbor in neighbors:
|
||||||
|
transport_type = neighbor.get('transport_type')
|
||||||
|
next_phase = allowed_next_phase(phase, transport_type)
|
||||||
|
if next_phase is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
travel_time = neighbor.get('travel_time_seconds')
|
||||||
|
distance_km = neighbor.get('distance_km')
|
||||||
|
|
||||||
|
neighbor_key = neighbor.get('neighbor_key')
|
||||||
|
node_docs[neighbor_key] = neighbor.get('neighbor_doc')
|
||||||
|
|
||||||
|
step_cost = travel_time if travel_time is not None else (distance_km or 0)
|
||||||
|
new_cost = cost + step_cost
|
||||||
|
|
||||||
|
state_key = (neighbor_key, next_phase)
|
||||||
|
if state_key in visited and new_cost >= visited[state_key]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
visited[state_key] = new_cost
|
||||||
|
counter += 1
|
||||||
|
heapq.heappush(queue, (new_cost, counter, neighbor_key, next_phase))
|
||||||
|
predecessors[state_key] = ((node_key, phase), neighbor)
|
||||||
|
|
||||||
|
logger.info("No route found from offer %s to hub %s", offer_uuid, hub_uuid)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
|
|||||||
Reference in New Issue
Block a user