Add graph route alternatives for external points
Build and deploy Docker image / build (push) Successful in 1m10s
Build and deploy Docker image / build (push) Successful in 1m10s
This commit is contained in:
+1
-1
Submodule graphql-contracts updated: 1af133dd20...99ecc3b719
@@ -16,10 +16,14 @@ export type ArangoDoc = Record<string, any>
|
||||
export interface RouteStage {
|
||||
fromUuid: string | null
|
||||
fromName: string | null
|
||||
fromCountryCode: string | null
|
||||
fromNodeType: string | null
|
||||
fromLat: number | null
|
||||
fromLon: number | null
|
||||
toUuid: string | null
|
||||
toName: string | null
|
||||
toCountryCode: string | null
|
||||
toNodeType: string | null
|
||||
toLat: number | null
|
||||
toLon: number | null
|
||||
distanceKm: number
|
||||
@@ -33,16 +37,25 @@ export interface RoutePath {
|
||||
stages: RouteStage[]
|
||||
}
|
||||
|
||||
export function normalizeRouteNodeType(nodeType: unknown): string | null {
|
||||
if (nodeType === 'logistics' || nodeType === null || nodeType === undefined) return 'hub'
|
||||
return String(nodeType)
|
||||
}
|
||||
|
||||
function buildStage(fromDoc: ArangoDoc | undefined, toDoc: ArangoDoc | undefined, transportType: string, edges: ArangoDoc[]): RouteStage {
|
||||
const distance = edges.reduce((s, e) => s + (e.distance_km || 0), 0)
|
||||
const time = edges.reduce((s, e) => s + (e.travel_time_seconds || 0), 0)
|
||||
return {
|
||||
fromUuid: fromDoc?._key ?? null,
|
||||
fromName: fromDoc?.name ?? null,
|
||||
fromCountryCode: fromDoc?.country_code ?? null,
|
||||
fromNodeType: normalizeRouteNodeType(fromDoc?.node_type),
|
||||
fromLat: fromDoc?.latitude ?? null,
|
||||
fromLon: fromDoc?.longitude ?? null,
|
||||
toUuid: toDoc?._key ?? null,
|
||||
toName: toDoc?.name ?? null,
|
||||
toCountryCode: toDoc?.country_code ?? null,
|
||||
toNodeType: normalizeRouteNodeType(toDoc?.node_type),
|
||||
toLat: toDoc?.latitude ?? null,
|
||||
toLon: toDoc?.longitude ?? null,
|
||||
distanceKm: distance,
|
||||
|
||||
+186
-1
@@ -1,6 +1,6 @@
|
||||
import { getDb, ensureGraph } from './db.js'
|
||||
import { getClusteredNodes } from './cluster.js'
|
||||
import { distanceKm, buildRouteFromEdges, type ArangoDoc, type RoutePath } from './helpers.js'
|
||||
import { distanceKm, buildRouteFromEdges, normalizeRouteNodeType, type ArangoDoc, type RoutePath, type RouteStage } from './helpers.js'
|
||||
|
||||
const GRAPHHOPPER_URL = process.env.GRAPHHOPPER_EXTERNAL_URL || 'https://graphhopper.optovia.ru'
|
||||
const OPENRAILROUTING_URL = process.env.OPENRAILROUTING_EXTERNAL_URL || 'https://openrailrouting.optovia.ru'
|
||||
@@ -45,10 +45,14 @@ export const typeDefs = `#graphql
|
||||
type RouteStage {
|
||||
fromUuid: String
|
||||
fromName: String
|
||||
fromCountryCode: String
|
||||
fromNodeType: String
|
||||
fromLat: Float
|
||||
fromLon: Float
|
||||
toUuid: String
|
||||
toName: String
|
||||
toCountryCode: String
|
||||
toNodeType: String
|
||||
toLat: Float
|
||||
toLon: Float
|
||||
distanceKm: Float
|
||||
@@ -207,6 +211,19 @@ export const typeDefs = `#graphql
|
||||
hubsForProduct(productUuid: String!, radiusKm: Float): [Node!]!
|
||||
offersByHub(hubUuid: String!, productUuid: String!, limit: Int): [ProductRouteOption!]!
|
||||
offerToHub(offerUuid: String!, hubUuid: String!): ProductRouteOption
|
||||
routesBetweenNodes(
|
||||
fromUuid: String!
|
||||
fromName: String
|
||||
fromCountryCode: String
|
||||
fromLat: Float
|
||||
fromLon: Float
|
||||
toUuid: String!
|
||||
toName: String
|
||||
toCountryCode: String
|
||||
toLat: Float
|
||||
toLon: Float
|
||||
limit: Int = 3
|
||||
): [RoutePath!]!
|
||||
nearestHubs(lat: Float!, lon: Float!, radius: Float, productUuid: String, limit: Int): [Node!]!
|
||||
nearestOffers(lat: Float!, lon: Float!, radius: Float, productUuid: String, hubUuid: String, limit: Int): [OfferWithRoute!]!
|
||||
nearestSuppliers(lat: Float!, lon: Float!, radius: Float, productUuid: String, limit: Int): [Supplier!]!
|
||||
@@ -233,6 +250,107 @@ function mapNode(doc: ArangoDoc, includeEdges = false) {
|
||||
}
|
||||
}
|
||||
|
||||
interface RouteEndpointArgs {
|
||||
uuid: string
|
||||
name?: string
|
||||
countryCode?: string
|
||||
lat?: number
|
||||
lon?: number
|
||||
}
|
||||
|
||||
async function resolveGraphRouteEndpoint(endpoint: RouteEndpointArgs): Promise<ArangoDoc> {
|
||||
const db = getDb()
|
||||
const exactCursor = await db.query(`
|
||||
LET exact = DOCUMENT(@document_id)
|
||||
RETURN exact
|
||||
`, { document_id: `nodes/${endpoint.uuid}` })
|
||||
const exact = await exactCursor.next()
|
||||
if (exact) return exact
|
||||
if (endpoint.lat === undefined || endpoint.lon === undefined) {
|
||||
throw new Error(`GEO node ${endpoint.uuid} does not exist and coordinates were not provided`)
|
||||
}
|
||||
const nearestCursor = await db.query(`
|
||||
FOR node IN nodes
|
||||
FILTER node.node_type == 'logistics' OR node.node_type == null
|
||||
FILTER node.latitude != null AND node.longitude != null
|
||||
SORT DISTANCE(node.latitude, node.longitude, @latitude, @longitude) ASC
|
||||
LIMIT 1
|
||||
RETURN node
|
||||
`, { latitude: endpoint.lat, longitude: endpoint.lon })
|
||||
const nearest = await nearestCursor.next()
|
||||
if (!nearest) throw new Error('GEO graph does not contain a route hub')
|
||||
return nearest
|
||||
}
|
||||
|
||||
function routeConnectorStage(
|
||||
endpoint: RouteEndpointArgs,
|
||||
graphNode: ArangoDoc,
|
||||
endpointIsSource: boolean,
|
||||
): RouteStage | null {
|
||||
if (endpoint.uuid === graphNode._key) return null
|
||||
if (endpoint.lat === undefined || endpoint.lon === undefined) return null
|
||||
const graphLat = graphNode.latitude as number | null
|
||||
const graphLon = graphNode.longitude as number | null
|
||||
if (graphLat === null || graphLon === null) return null
|
||||
const connectorDistance = distanceKm(endpoint.lat, endpoint.lon, graphLat, graphLon)
|
||||
const endpointData = {
|
||||
uuid: endpoint.uuid,
|
||||
name: endpoint.name ?? endpoint.uuid,
|
||||
countryCode: endpoint.countryCode ?? null,
|
||||
lat: endpoint.lat,
|
||||
lon: endpoint.lon,
|
||||
nodeType: 'external',
|
||||
}
|
||||
const graphData = {
|
||||
uuid: graphNode._key as string,
|
||||
name: (graphNode.name as string | undefined) ?? graphNode._key as string,
|
||||
countryCode: (graphNode.country_code as string | undefined) ?? null,
|
||||
lat: graphLat,
|
||||
lon: graphLon,
|
||||
nodeType: normalizeRouteNodeType(graphNode.node_type),
|
||||
}
|
||||
const from = endpointIsSource ? endpointData : graphData
|
||||
const to = endpointIsSource ? graphData : endpointData
|
||||
return {
|
||||
fromUuid: from.uuid,
|
||||
fromName: from.name,
|
||||
fromCountryCode: from.countryCode,
|
||||
fromNodeType: from.nodeType,
|
||||
fromLat: from.lat,
|
||||
fromLon: from.lon,
|
||||
toUuid: to.uuid,
|
||||
toName: to.name,
|
||||
toCountryCode: to.countryCode,
|
||||
toNodeType: to.nodeType,
|
||||
toLat: to.lat,
|
||||
toLon: to.lon,
|
||||
distanceKm: connectorDistance,
|
||||
travelTimeSeconds: Math.round((connectorDistance / 60) * 3600),
|
||||
transportType: 'road',
|
||||
}
|
||||
}
|
||||
|
||||
function withRouteConnectors(
|
||||
route: RoutePath,
|
||||
fromEndpoint: RouteEndpointArgs,
|
||||
fromNode: ArangoDoc,
|
||||
toEndpoint: RouteEndpointArgs,
|
||||
toNode: ArangoDoc,
|
||||
): RoutePath {
|
||||
const firstMile = routeConnectorStage(fromEndpoint, fromNode, true)
|
||||
const lastMile = routeConnectorStage(toEndpoint, toNode, false)
|
||||
const stages = [
|
||||
...(firstMile ? [firstMile] : []),
|
||||
...route.stages,
|
||||
...(lastMile ? [lastMile] : []),
|
||||
]
|
||||
return {
|
||||
stages,
|
||||
totalDistanceKm: stages.reduce((sum, stage) => sum + stage.distanceKm, 0),
|
||||
totalTimeSeconds: stages.reduce((sum, stage) => sum + stage.travelTimeSeconds, 0),
|
||||
}
|
||||
}
|
||||
|
||||
function productView(doc: ArangoDoc) {
|
||||
const productUuid = doc.product_uuid ?? doc.uuid ?? null
|
||||
return {
|
||||
@@ -1111,6 +1229,73 @@ export const resolvers = {
|
||||
return resolveOfferToHubInternal(args.offerUuid, args.hubUuid)
|
||||
},
|
||||
|
||||
routesBetweenNodes: async (_: unknown, args: {
|
||||
fromUuid: string
|
||||
fromName?: string
|
||||
fromCountryCode?: string
|
||||
fromLat?: number
|
||||
fromLon?: number
|
||||
toUuid: string
|
||||
toName?: string
|
||||
toCountryCode?: string
|
||||
toLat?: number
|
||||
toLon?: number
|
||||
limit?: number
|
||||
}) => {
|
||||
const db = getDb()
|
||||
await ensureGraph()
|
||||
const limit = Math.min(Math.max(args.limit ?? 3, 1), 3)
|
||||
const fromEndpoint: RouteEndpointArgs = {
|
||||
uuid: args.fromUuid,
|
||||
name: args.fromName,
|
||||
countryCode: args.fromCountryCode,
|
||||
lat: args.fromLat,
|
||||
lon: args.fromLon,
|
||||
}
|
||||
const toEndpoint: RouteEndpointArgs = {
|
||||
uuid: args.toUuid,
|
||||
name: args.toName,
|
||||
countryCode: args.toCountryCode,
|
||||
lat: args.toLat,
|
||||
lon: args.toLon,
|
||||
}
|
||||
const fromNode = await resolveGraphRouteEndpoint(fromEndpoint)
|
||||
const toNode = await resolveGraphRouteEndpoint(toEndpoint)
|
||||
const cursor = await db.query(`
|
||||
FOR path IN ANY K_SHORTEST_PATHS @from_id TO @to_id
|
||||
GRAPH 'optovia_graph'
|
||||
OPTIONS {
|
||||
weightAttribute: 'travel_time_seconds',
|
||||
defaultWeight: 1
|
||||
}
|
||||
LIMIT @limit
|
||||
RETURN path
|
||||
`, {
|
||||
from_id: fromNode._id,
|
||||
to_id: toNode._id,
|
||||
limit,
|
||||
})
|
||||
const paths = await cursor.all()
|
||||
return paths
|
||||
.map((path: ArangoDoc) => {
|
||||
const nodeDocs = new Map<string, ArangoDoc>(
|
||||
path.vertices.map((vertex: ArangoDoc) => [vertex._key as string, vertex]),
|
||||
)
|
||||
const pathEdges: [string, string, ArangoDoc][] = path.edges.map(
|
||||
(edge: ArangoDoc, index: number) => [
|
||||
path.vertices[index]._key as string,
|
||||
path.vertices[index + 1]._key as string,
|
||||
edge,
|
||||
],
|
||||
)
|
||||
const route = buildRouteFromEdges(pathEdges, nodeDocs)
|
||||
return route
|
||||
? withRouteConnectors(route, fromEndpoint, fromNode, toEndpoint, toNode)
|
||||
: null
|
||||
})
|
||||
.filter((route: RoutePath | null): route is RoutePath => route !== null)
|
||||
},
|
||||
|
||||
nearestHubs: async (_: unknown, args: { lat: number; lon: number; radius?: number; productUuid?: string; limit?: number }) => {
|
||||
const db = getDb()
|
||||
const radius = args.radius ?? 1000
|
||||
|
||||
Reference in New Issue
Block a user