This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { createHash } from 'node:crypto'
|
import { createHash } from 'node:crypto'
|
||||||
import { readFileSync } from 'node:fs'
|
import { readFileSync } from 'node:fs'
|
||||||
import { ensureGraph, getDb } from '../dist/db.js'
|
import { ensureGraph, getDb } from '../dist/db.js'
|
||||||
|
import { AFRICA_MARITIME_LOCODES } from '../dist/maritime.js'
|
||||||
|
|
||||||
type SupplierSeed = { lei: string; name: string; countryCode: string; city: string }
|
type SupplierSeed = { lei: string; name: string; countryCode: string; city: string }
|
||||||
type HubSeed = {
|
type HubSeed = {
|
||||||
@@ -155,6 +156,7 @@ const hubNodes = hubs.map((hub) => {
|
|||||||
country,
|
country,
|
||||||
country_code: hub.countryCode,
|
country_code: hub.countryCode,
|
||||||
transport_types: transportTypes(hub.functionCode),
|
transport_types: transportTypes(hub.functionCode),
|
||||||
|
is_maritime_port: AFRICA_MARITIME_LOCODES.has(hub.code),
|
||||||
navigation_priority: priorityHubCodes.has(hub.code) ? 1 : 0,
|
navigation_priority: priorityHubCodes.has(hub.code) ? 1 : 0,
|
||||||
source: 'UN/LOCODE Africa',
|
source: 'UN/LOCODE Africa',
|
||||||
locode: hub.code,
|
locode: hub.code,
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
import { isMaritimePort } from './maritime.js'
|
||||||
|
|
||||||
|
test('accepts curated coastal LOCODE ports', () => {
|
||||||
|
assert.equal(isMaritimePort({ locode: 'NGLAG' }), true)
|
||||||
|
assert.equal(isMaritimePort({ locode: 'CDBNW' }), true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('rejects inland ports even when UN/LOCODE function 1 is present', () => {
|
||||||
|
assert.equal(isMaritimePort({ locode: 'CMGOU', function_code: '1-34----' }), false)
|
||||||
|
assert.equal(isMaritimePort({ locode: 'CGOYO', function_code: '1-3-----' }), false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('accepts an explicitly curated database record', () => {
|
||||||
|
assert.equal(isMaritimePort({ locode: 'CUSTOM', is_maritime_port: true }), true)
|
||||||
|
})
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
import type { ArangoDoc } from './helpers.js'
|
||||||
|
|
||||||
|
// UN/LOCODE function 1 also includes inland waterway ports. This curated set
|
||||||
|
// keeps the maritime planner on coastal and ocean-accessible terminals only.
|
||||||
|
export const AFRICA_MARITIME_LOCODES = new Set([
|
||||||
|
'BJPTN',
|
||||||
|
'CDBNW',
|
||||||
|
'CIASS',
|
||||||
|
'CIBAO',
|
||||||
|
'CIDAB',
|
||||||
|
'CIFRE',
|
||||||
|
'CIGLU',
|
||||||
|
'CISPY',
|
||||||
|
'CMLIM',
|
||||||
|
'CMMOU',
|
||||||
|
'CVMIN',
|
||||||
|
'DJDMJ',
|
||||||
|
'DJPOD',
|
||||||
|
'DJDCT',
|
||||||
|
'DZALG',
|
||||||
|
'DZAZW',
|
||||||
|
'DZCHE',
|
||||||
|
'DZCOL',
|
||||||
|
'DZDEL',
|
||||||
|
'DZGHZ',
|
||||||
|
'EGAGN',
|
||||||
|
'EGADA',
|
||||||
|
'EGAQU',
|
||||||
|
'EGSUZ',
|
||||||
|
'EGDOM',
|
||||||
|
'EGDAM',
|
||||||
|
'EGGUA',
|
||||||
|
'EGGAL',
|
||||||
|
'EGIKU',
|
||||||
|
'EGEPP',
|
||||||
|
'EGOPT',
|
||||||
|
'EGGLB',
|
||||||
|
'EGPSE',
|
||||||
|
'EGMEA',
|
||||||
|
'EGSBY',
|
||||||
|
'ERASA',
|
||||||
|
'ERMSW',
|
||||||
|
'GNBEL',
|
||||||
|
'GNBFA',
|
||||||
|
'GNDPL',
|
||||||
|
'GNPMT',
|
||||||
|
'GNDKA',
|
||||||
|
'GNKMC',
|
||||||
|
'GWOXB',
|
||||||
|
'GWBOL',
|
||||||
|
'GWBQE',
|
||||||
|
'GWCAC',
|
||||||
|
'KESMN',
|
||||||
|
'KMAJN',
|
||||||
|
'LRHAR',
|
||||||
|
'LRMLW',
|
||||||
|
'LYKHO',
|
||||||
|
'LYELK',
|
||||||
|
'LYMEL',
|
||||||
|
'MAANZ',
|
||||||
|
'MABUJ',
|
||||||
|
'MAELJ',
|
||||||
|
'MAESS',
|
||||||
|
'MARNA',
|
||||||
|
'MGNOS',
|
||||||
|
'MUPMA',
|
||||||
|
'MZCOR',
|
||||||
|
'MZMZB',
|
||||||
|
'NGFLM',
|
||||||
|
'NGLAG',
|
||||||
|
'NGLKK',
|
||||||
|
'NGONN',
|
||||||
|
'SCVIC',
|
||||||
|
'SLKSY',
|
||||||
|
'SOELM',
|
||||||
|
'SOGRC',
|
||||||
|
'STTMS',
|
||||||
|
'TNHMM',
|
||||||
|
'TNTZE',
|
||||||
|
'TNBJC',
|
||||||
|
'TNCHB',
|
||||||
|
'TNMAD',
|
||||||
|
'TNRDS',
|
||||||
|
'TNSLN',
|
||||||
|
'TNZRZ',
|
||||||
|
'TZDAR',
|
||||||
|
'TZIKW',
|
||||||
|
'ZACPT',
|
||||||
|
'ZADUR',
|
||||||
|
'ZAKOM',
|
||||||
|
'ZAPNL',
|
||||||
|
'ZAPED',
|
||||||
|
'ZAJOH',
|
||||||
|
'ZAZE4',
|
||||||
|
'ZASMN',
|
||||||
|
'ZASTD',
|
||||||
|
])
|
||||||
|
|
||||||
|
export function isMaritimePort(document: ArangoDoc): boolean {
|
||||||
|
return document.is_maritime_port === true
|
||||||
|
|| AFRICA_MARITIME_LOCODES.has(String(document.locode ?? '').toUpperCase())
|
||||||
|
}
|
||||||
+8
-4
@@ -378,18 +378,18 @@ async function maritimeRoutesBetweenNodes(
|
|||||||
FOR port IN nodes
|
FOR port IN nodes
|
||||||
FILTER port.node_type == 'logistics' OR port.node_type == null
|
FILTER port.node_type == 'logistics' OR port.node_type == null
|
||||||
FILTER port.latitude != null AND port.longitude != null
|
FILTER port.latitude != null AND port.longitude != null
|
||||||
FILTER 'sea' IN port.transport_types
|
FILTER port.is_maritime_port == true
|
||||||
SORT DISTANCE(@source_lat, @source_lon, port.latitude, port.longitude) ASC
|
SORT DISTANCE(@source_lat, @source_lon, port.latitude, port.longitude) ASC
|
||||||
LIMIT 8
|
LIMIT 12
|
||||||
RETURN port
|
RETURN port
|
||||||
)
|
)
|
||||||
LET destination_ports = (
|
LET destination_ports = (
|
||||||
FOR port IN nodes
|
FOR port IN nodes
|
||||||
FILTER port.node_type == 'logistics' OR port.node_type == null
|
FILTER port.node_type == 'logistics' OR port.node_type == null
|
||||||
FILTER port.latitude != null AND port.longitude != null
|
FILTER port.latitude != null AND port.longitude != null
|
||||||
FILTER 'sea' IN port.transport_types
|
FILTER port.is_maritime_port == true
|
||||||
SORT DISTANCE(@destination_lat, @destination_lon, port.latitude, port.longitude) ASC
|
SORT DISTANCE(@destination_lat, @destination_lon, port.latitude, port.longitude) ASC
|
||||||
LIMIT 8
|
LIMIT 12
|
||||||
RETURN port
|
RETURN port
|
||||||
)
|
)
|
||||||
FOR origin_port IN origin_ports
|
FOR origin_port IN origin_ports
|
||||||
@@ -398,6 +398,10 @@ async function maritimeRoutesBetweenNodes(
|
|||||||
LET first_mile = DISTANCE(@source_lat, @source_lon, origin_port.latitude, origin_port.longitude)
|
LET first_mile = DISTANCE(@source_lat, @source_lon, origin_port.latitude, origin_port.longitude)
|
||||||
LET sea_leg = DISTANCE(origin_port.latitude, origin_port.longitude, destination_port.latitude, destination_port.longitude)
|
LET sea_leg = DISTANCE(origin_port.latitude, origin_port.longitude, destination_port.latitude, destination_port.longitude)
|
||||||
LET last_mile = DISTANCE(destination_port.latitude, destination_port.longitude, @destination_lat, @destination_lon)
|
LET last_mile = DISTANCE(destination_port.latitude, destination_port.longitude, @destination_lat, @destination_lon)
|
||||||
|
LET origin_to_destination = DISTANCE(@destination_lat, @destination_lon, origin_port.latitude, origin_port.longitude)
|
||||||
|
LET destination_to_source = DISTANCE(@source_lat, @source_lon, destination_port.latitude, destination_port.longitude)
|
||||||
|
FILTER first_mile < origin_to_destination
|
||||||
|
FILTER last_mile < destination_to_source
|
||||||
SORT first_mile + sea_leg + last_mile ASC
|
SORT first_mile + sea_leg + last_mile ASC
|
||||||
LIMIT @limit
|
LIMIT @limit
|
||||||
RETURN { origin_port, destination_port }
|
RETURN { origin_port, destination_port }
|
||||||
|
|||||||
Reference in New Issue
Block a user