From c759e81c23ab367d0e20ef27afae224303cd7b2f Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 19 Apr 2023 17:32:42 +0200 Subject: [PATCH] WIP graphhopper georouter --- package-lock.json | 266 ++++++++++++++++- package.json | 4 + .../matcher/adapters/secondaries/geodesic.ts | 27 ++ .../adapters/secondaries/georouter-creator.ts | 8 +- .../secondaries/graphhopper-georouter.ts | 274 +++++++++++++++--- src/modules/matcher/domain/entities/actor.ts | 9 + src/modules/matcher/domain/entities/route.ts | 56 +++- .../domain/entities/spacetime-point.ts | 11 + .../matcher/domain/entities/waypoint.ts | 6 + .../domain/interfaces/geodesic.interface.ts | 11 + .../matcher/domain/usecases/match.usecase.ts | 87 +++--- src/modules/matcher/matcher.module.ts | 2 + .../default-params.provider.spec.ts | 4 +- .../adapters/secondaries/geodesic.spec.ts | 14 + .../secondaries}/georouter-creator.spec.ts | 10 +- .../secondaries/graphhopper-georouter.spec.ts | 268 +++++++++++++++++ .../secondaries}/messager.spec.ts | 2 +- .../tests/unit/{ => domain}/geography.spec.ts | 2 +- .../unit/{ => domain}/match.usecase.spec.ts | 14 +- .../tests/unit/{ => domain}/person.spec.ts | 2 +- .../matcher/tests/unit/domain/route.spec.ts | 55 ++++ .../tests/unit/{ => domain}/time.spec.ts | 2 +- .../tests/unit/graphhopper-georouter.spec.ts | 141 --------- .../unit/{ => queries}/match.query.spec.ts | 12 +- 24 files changed, 1033 insertions(+), 254 deletions(-) create mode 100644 src/modules/matcher/adapters/secondaries/geodesic.ts create mode 100644 src/modules/matcher/domain/entities/actor.ts create mode 100644 src/modules/matcher/domain/entities/spacetime-point.ts create mode 100644 src/modules/matcher/domain/entities/waypoint.ts create mode 100644 src/modules/matcher/domain/interfaces/geodesic.interface.ts rename src/modules/matcher/tests/unit/{ => adapters/secondaries}/default-params.provider.spec.ts (84%) create mode 100644 src/modules/matcher/tests/unit/adapters/secondaries/geodesic.spec.ts rename src/modules/matcher/tests/unit/{ => adapters/secondaries}/georouter-creator.spec.ts (72%) create mode 100644 src/modules/matcher/tests/unit/adapters/secondaries/graphhopper-georouter.spec.ts rename src/modules/matcher/tests/unit/{ => adapters/secondaries}/messager.spec.ts (94%) rename src/modules/matcher/tests/unit/{ => domain}/geography.spec.ts (97%) rename src/modules/matcher/tests/unit/{ => domain}/match.usecase.spec.ts (80%) rename src/modules/matcher/tests/unit/{ => domain}/person.spec.ts (94%) create mode 100644 src/modules/matcher/tests/unit/domain/route.spec.ts rename src/modules/matcher/tests/unit/{ => domain}/time.spec.ts (98%) delete mode 100644 src/modules/matcher/tests/unit/graphhopper-georouter.spec.ts rename src/modules/matcher/tests/unit/{ => queries}/match.query.spec.ts (93%) diff --git a/package-lock.json b/package-lock.json index 3efcc5e..8a64409 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,8 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "geo-tz": "^7.0.7", + "geographiclib-geodesic": "^2.0.0", + "got": "^11.8.6", "ioredis": "^5.3.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0" @@ -2129,6 +2131,17 @@ "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", "dev": true }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@sinonjs/commons": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", @@ -2147,6 +2160,17 @@ "@sinonjs/commons": "^2.0.0" } }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -2253,6 +2277,17 @@ "@types/node": "*" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -2326,6 +2361,11 @@ "@types/node": "*" } }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -2366,6 +2406,14 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -2406,6 +2454,14 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "dev": true }, + "node_modules/@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/semver": { "version": "7.3.13", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", @@ -3493,6 +3549,45 @@ "node": ">=12" } }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -3735,6 +3830,17 @@ "node": ">=0.8" } }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -3939,6 +4045,31 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -3972,6 +4103,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "engines": { + "node": ">=10" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -4122,7 +4261,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "dependencies": { "once": "^1.4.0" } @@ -5016,6 +5154,11 @@ "node": ">= 6" } }, + "node_modules/geographiclib-geodesic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/geographiclib-geodesic/-/geographiclib-geodesic-2.0.0.tgz", + "integrity": "sha512-qRE11UEF3Zn9VwDFf+Q1ZNn4VW2xwZWeAPiFRrKVSKn2K5lds1jOxhxgFJwbKh5YV58ME6+LGiRtm4A0CjFyiQ==" + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -5131,6 +5274,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -5188,6 +5355,11 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -5203,6 +5375,18 @@ "node": ">= 0.8" } }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -6215,6 +6399,11 @@ "node": ">=4" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -6263,6 +6452,14 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/keyv": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", + "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -6387,6 +6584,14 @@ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "engines": { + "node": ">=8" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -6559,6 +6764,14 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -6725,6 +6938,17 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -6768,7 +6992,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -6853,6 +7076,14 @@ "node": ">=0.10.0" } }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "engines": { + "node": ">=8" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -7279,7 +7510,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -7349,6 +7579,17 @@ } ] }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -7492,6 +7733,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, "node_modules/resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -7539,6 +7785,17 @@ "node": ">=10" } }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -8956,8 +9213,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/write-file-atomic": { "version": "4.0.2", diff --git a/package.json b/package.json index e304cb9..34f162f 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,8 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "geo-tz": "^7.0.7", + "geographiclib-geodesic": "^2.0.0", + "got": "^11.8.6", "ioredis": "^5.3.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0" @@ -95,6 +97,7 @@ ".presenter.ts", ".profile.ts", ".exception.ts", + ".enum.ts", "main.ts", "prisma-service.ts" ], @@ -113,6 +116,7 @@ ".presenter.ts", ".profile.ts", ".exception.ts", + ".enum.ts", "main.ts", "prisma-service.ts" ], diff --git a/src/modules/matcher/adapters/secondaries/geodesic.ts b/src/modules/matcher/adapters/secondaries/geodesic.ts new file mode 100644 index 0000000..56423a8 --- /dev/null +++ b/src/modules/matcher/adapters/secondaries/geodesic.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@nestjs/common'; +import { IGeodesic } from '../../domain/interfaces/geodesic.interface'; +import { Geodesic, GeodesicClass } from 'geographiclib-geodesic'; + +@Injectable() +export class MatcherGeodesic implements IGeodesic { + _geod: GeodesicClass; + + constructor() { + this._geod = Geodesic.WGS84; + } + + inverse( + lon1: number, + lat1: number, + lon2: number, + lat2: number, + ): { azimuth: number; distance: number } { + const { azi2: azimuth, s12: distance } = this._geod.Inverse( + lat1, + lon1, + lat2, + lon2, + ); + return { azimuth, distance }; + } +} diff --git a/src/modules/matcher/adapters/secondaries/georouter-creator.ts b/src/modules/matcher/adapters/secondaries/georouter-creator.ts index e87c1fa..22e65b3 100644 --- a/src/modules/matcher/adapters/secondaries/georouter-creator.ts +++ b/src/modules/matcher/adapters/secondaries/georouter-creator.ts @@ -3,15 +3,19 @@ import { ICreateGeorouter } from '../../domain/interfaces/georouter-creator.inte import { IGeorouter } from '../../domain/interfaces/georouter.interface'; import { GraphhopperGeorouter } from './graphhopper-georouter'; import { HttpService } from '@nestjs/axios'; +import { MatcherGeodesic } from './geodesic'; @Injectable() export class GeorouterCreator implements ICreateGeorouter { - constructor(private readonly httpService: HttpService) {} + constructor( + private readonly httpService: HttpService, + private readonly geodesic: MatcherGeodesic, + ) {} create(type: string, url: string): IGeorouter { switch (type) { case 'graphhopper': - return new GraphhopperGeorouter(url, this.httpService); + return new GraphhopperGeorouter(url, this.httpService, this.geodesic); default: throw new Error('Unknown geocoder'); } diff --git a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts index d08583e..87e2129 100644 --- a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts +++ b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts @@ -4,18 +4,11 @@ import { IGeorouter } from '../../domain/interfaces/georouter.interface'; import { GeorouterSettings } from '../../domain/types/georouter-settings.type'; import { Path } from '../../domain/types/path.type'; import { Injectable } from '@nestjs/common'; -import { - catchError, - defer, - forkJoin, - from, - lastValueFrom, - map, - mergeAll, - mergeMap, - toArray, -} from 'rxjs'; -import { AxiosError } from 'axios'; +import { catchError, lastValueFrom, map } from 'rxjs'; +import { AxiosError, AxiosResponse } from 'axios'; +import { Route } from '../../domain/entities/route'; +import { SpacetimePoint } from '../../domain/entities/spacetime-point'; +import { IGeodesic } from '../../domain/interfaces/geodesic.interface'; @Injectable() export class GraphhopperGeorouter implements IGeorouter { @@ -26,10 +19,12 @@ export class GraphhopperGeorouter implements IGeorouter { _withDistance: boolean; _paths: Array; _httpService: HttpService; + _geodesic: IGeodesic; - constructor(url: string, httpService: HttpService) { + constructor(url: string, httpService: HttpService, geodesic: IGeodesic) { this._url = url + '/route?'; this._httpService = httpService; + this._geodesic = geodesic; } async route( @@ -41,9 +36,7 @@ export class GraphhopperGeorouter implements IGeorouter { this._setWithPoints(settings.withPoints); this._setWithDistance(settings.withDistance); this._paths = paths; - const routes = await this._getRoutes(); - console.log(routes.length); - return routes; + return await this._getRoutes(); } _setDefaultUrlArgs(): void { @@ -63,7 +56,7 @@ export class GraphhopperGeorouter implements IGeorouter { _setWithPoints(withPoints: boolean): void { this._withPoints = withPoints; - if (withPoints) { + if (!withPoints) { this._urlArgs.push('calc_points=false'); } } @@ -87,9 +80,11 @@ export class GraphhopperGeorouter implements IGeorouter { .map((point) => [point.lat, point.lon].join()) .join('&point='), ].join(''); - const res = await lastValueFrom( + const route = await lastValueFrom( this._httpService.get(url).pipe( - map((res) => res.data.paths[0].distance), + map((res) => + res.data ? this._createRoute(path.key, res) : undefined, + ), catchError((error: AxiosError) => { throw new Error(error.message); }), @@ -97,37 +92,230 @@ export class GraphhopperGeorouter implements IGeorouter { ); return { key: path.key, - route: res, + route, }; }), ); return routes; - // const date1 = new Date(); - // const urls = this._paths.map((path) => - // defer(() => - // this._httpService - // .get( - // [ - // this._getUrl(), - // '&point=', - // path.points - // .map((point) => [point.lat, point.lon].join()) - // .join('&point='), - // ].join(''), - // ) - // .pipe(map((res) => res.data.paths[0].distance)), - // ), - // ); - // const observables = from(urls); - // const routes = observables.pipe(mergeAll(7), toArray()); - // routes.subscribe(() => { - // const date2 = new Date(); - // console.log(date2.getTime() - date1.getTime()); - // }); - // return []; } _getUrl(): string { return [this._url, this._urlArgs.join('&')].join(''); } + + _createRoute( + key: string, + response: AxiosResponse, + ): Route { + const route = new Route(this._geodesic); + if (response.data.paths && response.data.paths[0]) { + const shortestPath = response.data.paths[0]; + route.distance = shortestPath.distance ?? 0; + route.duration = shortestPath.time ? shortestPath.time / 1000 : 0; + if (shortestPath.points && shortestPath.points.coordinates) { + route.setPoints(shortestPath.points.coordinates); + if ( + shortestPath.details && + shortestPath.details.time && + shortestPath.snapped_waypoints && + shortestPath.snapped_waypoints.coordinates + ) { + let instructions: Array = []; + if (shortestPath.instructions) + instructions = shortestPath.instructions; + route.setSpacetimePoints( + this._generateSpacetimePoints( + shortestPath.points.coordinates, + shortestPath.snapped_waypoints.coordinates, + shortestPath.details.time, + instructions, + ), + ); + } + } + } + return route; + } + + _generateSpacetimePoints( + points: Array>, + snappedWaypoints: Array>, + durations: Array>, + instructions: Array, + ): Array { + const indices = this._getIndices(points, snappedWaypoints); + const times = this._getTimes(durations, indices); + const distances = this._getDistances(instructions, indices); + return indices.map( + (index) => + new SpacetimePoint( + points[index], + times.find((time) => time.index == index)?.duration, + distances.find((distance) => distance.index == index)?.distance, + ), + ); + } + + _getIndices( + points: Array>, + snappedWaypoints: Array>, + ): Array { + const indices = snappedWaypoints.map((waypoint) => + points.findIndex( + (point) => point[0] == waypoint[0] && point[1] == waypoint[1], + ), + ); + if (indices.find((index) => index == -1) === undefined) return indices; + const missedWaypoints = indices + .map( + (value, index) => + < + { + index: number; + originIndex: number; + waypoint: Array; + nearest: number; + distance: number; + } + >{ + index: value, + originIndex: index, + waypoint: snappedWaypoints[index], + nearest: undefined, + distance: 999999999, + }, + ) + .filter((element) => element.index == -1); + for (const index in points) { + for (const missedWaypoint of missedWaypoints) { + const inverse = this._geodesic.inverse( + missedWaypoint.waypoint[0], + missedWaypoint.waypoint[1], + points[index][0], + points[index][1], + ); + if (inverse.distance < missedWaypoint.distance) { + missedWaypoint.distance = inverse.distance; + missedWaypoint.nearest = parseInt(index); + } + } + } + for (const missedWaypoint of missedWaypoints) { + indices[missedWaypoint.originIndex] = missedWaypoint.nearest; + } + return indices; + } + + _getTimes( + durations: Array>, + indices: Array, + ): Array<{ index: number; duration: number }> { + const times: Array<{ index: number; duration: number }> = []; + let duration = 0; + for (const [origin, destination, stepDuration] of durations) { + let indexFound = false; + const indexAsOrigin = indices.find((index) => index == origin); + if ( + indexAsOrigin !== undefined && + times.find((time) => origin == time.index) == undefined + ) { + times.push({ + index: indexAsOrigin, + duration: Math.round(stepDuration / 1000), + }); + indexFound = true; + } + if (!indexFound) { + const indexAsDestination = indices.find( + (index) => index == destination, + ); + if ( + indexAsDestination !== undefined && + times.find((time) => destination == time.index) == undefined + ) { + times.push({ + index: indexAsDestination, + duration: Math.round((duration + stepDuration) / 1000), + }); + indexFound = true; + } + } + if (!indexFound) { + const indexInBetween = indices.find( + (index) => origin < index && index < destination, + ); + if (indexInBetween !== undefined) { + times.push({ + index: indexInBetween, + duration: Math.round((duration + stepDuration / 2) / 1000), + }); + } + } + duration += stepDuration; + } + return times; + } + + _getDistances( + instructions: Array, + indices: Array, + ): Array<{ index: number; distance: number }> { + let distance = 0; + const distances: Array<{ index: number; distance: number }> = [ + { + index: 0, + distance, + }, + ]; + for (const instruction of instructions) { + distance += instruction.distance; + if ( + (instruction.sign == GraphhopperSign.SIGN_WAYPOINT || + instruction.sign == GraphhopperSign.SIGN_FINISH) && + indices.find((index) => index == instruction.interval[0]) !== undefined + ) { + distances.push({ + index: instruction.interval[0], + distance: Math.round(distance), + }); + } + } + return distances; + } +} + +type GraphhopperResponse = { + paths: [ + { + distance: number; + weight: number; + time: number; + points_encoded: boolean; + bbox: Array; + points: GraphhopperCoordinates; + snapped_waypoints: GraphhopperCoordinates; + details: { + time: Array>; + }; + instructions: Array; + }, + ]; +}; + +type GraphhopperCoordinates = { + coordinates: Array>; +}; + +type GraphhopperInstruction = { + distance: number; + heading: number; + sign: GraphhopperSign; + interval: Array; + text: string; +}; + +enum GraphhopperSign { + SIGN_START = 0, + SIGN_FINISH = 4, + SIGN_WAYPOINT = 5, } diff --git a/src/modules/matcher/domain/entities/actor.ts b/src/modules/matcher/domain/entities/actor.ts new file mode 100644 index 0000000..de9448a --- /dev/null +++ b/src/modules/matcher/domain/entities/actor.ts @@ -0,0 +1,9 @@ +import { Role } from '../types/role.enum'; +import { Step } from '../types/step.enum'; +import { Person } from './person'; + +export class Actor { + person: Person; + role: Role; + step: Step; +} diff --git a/src/modules/matcher/domain/entities/route.ts b/src/modules/matcher/domain/entities/route.ts index bf729c9..324abec 100644 --- a/src/modules/matcher/domain/entities/route.ts +++ b/src/modules/matcher/domain/entities/route.ts @@ -1 +1,55 @@ -export class Route {} +import { IGeodesic } from '../interfaces/geodesic.interface'; +import { SpacetimePoint } from './spacetime-point'; +import { Waypoint } from './waypoint'; + +export class Route { + distance: number; + duration: number; + fwdAzimuth: number; + backAzimuth: number; + distanceAzimuth: number; + waypoints: Array; + points: Array>; + spacetimePoints: Array; + _geodesic: IGeodesic; + + constructor(geodesic: IGeodesic) { + this.distance = undefined; + this.duration = undefined; + this.fwdAzimuth = undefined; + this.backAzimuth = undefined; + this.distanceAzimuth = undefined; + this.waypoints = []; + this.points = []; + this.spacetimePoints = []; + this._geodesic = geodesic; + } + + setWaypoints(waypoints: Array): void { + this.waypoints = waypoints; + this._setAzimuth(waypoints.map((waypoint) => waypoint.point)); + } + + setPoints(points: Array>): void { + this.points = points; + this._setAzimuth(points); + } + + setSpacetimePoints(spacetimePoints: Array): void { + this.spacetimePoints = spacetimePoints; + } + + _setAzimuth(points: Array>): void { + const inverse = this._geodesic.inverse( + points[0][0], + points[0][1], + points[points.length - 1][0], + points[points.length - 1][1], + ); + this.fwdAzimuth = + inverse.azimuth >= 0 ? inverse.azimuth : 360 - Math.abs(inverse.azimuth); + this.backAzimuth = + this.fwdAzimuth > 180 ? this.fwdAzimuth - 180 : this.fwdAzimuth + 180; + this.distanceAzimuth = inverse.distance; + } +} diff --git a/src/modules/matcher/domain/entities/spacetime-point.ts b/src/modules/matcher/domain/entities/spacetime-point.ts new file mode 100644 index 0000000..98fe80f --- /dev/null +++ b/src/modules/matcher/domain/entities/spacetime-point.ts @@ -0,0 +1,11 @@ +export class SpacetimePoint { + point: Array; + duration: number; + distance: number; + + constructor(point: Array, duration: number, distance: number) { + this.point = point; + this.duration = duration; + this.distance = distance; + } +} diff --git a/src/modules/matcher/domain/entities/waypoint.ts b/src/modules/matcher/domain/entities/waypoint.ts new file mode 100644 index 0000000..f44773a --- /dev/null +++ b/src/modules/matcher/domain/entities/waypoint.ts @@ -0,0 +1,6 @@ +import { Actor } from './actor'; + +export class Waypoint { + point: Array; + actors: Array; +} diff --git a/src/modules/matcher/domain/interfaces/geodesic.interface.ts b/src/modules/matcher/domain/interfaces/geodesic.interface.ts new file mode 100644 index 0000000..95680e8 --- /dev/null +++ b/src/modules/matcher/domain/interfaces/geodesic.interface.ts @@ -0,0 +1,11 @@ +export interface IGeodesic { + inverse( + lon1: number, + lat1: number, + lon2: number, + lat2: number, + ): { + azimuth: number; + distance: number; + }; +} diff --git a/src/modules/matcher/domain/usecases/match.usecase.ts b/src/modules/matcher/domain/usecases/match.usecase.ts index f67641b..6d48e63 100644 --- a/src/modules/matcher/domain/usecases/match.usecase.ts +++ b/src/modules/matcher/domain/usecases/match.usecase.ts @@ -17,54 +17,59 @@ export class MatchUseCase { async execute(matchQuery: MatchQuery): Promise> { try { - const paths = []; - for (let i = 0; i < 2000; i++) { - paths.push({ - key: 'route' + i, - points: [ - { - lat: 48.110899, - lon: -1.68365, - }, - { - lat: 48.131105, - lon: -1.690067, - }, - { - lat: 48.56516, - lon: -1.923553, - }, - { - lat: 48.622813, - lon: -1.997177, - }, - { - lat: 48.67846, - lon: -1.8554, - }, - ], - }); - } - const routes = await matchQuery.algorithmSettings.georouter.route(paths, { - withDistance: true, - withPoints: true, - withTime: false, - }); + // const paths = []; + // for (let i = 0; i < 1; i++) { + // paths.push({ + // key: 'route' + i, + // points: [ + // { + // lat: 48.110899, + // lon: -1.68365, + // }, + // { + // lat: 48.131105, + // lon: -1.690067, + // }, + // { + // lat: 48.534769, + // lon: -1.894032, + // }, + // { + // lat: 48.56516, + // lon: -1.923553, + // }, + // { + // lat: 48.622813, + // lon: -1.997177, + // }, + // { + // lat: 48.67846, + // lon: -1.8554, + // }, + // ], + // }); + // } + // const routes = await matchQuery.algorithmSettings.georouter.route(paths, { + // withDistance: false, + // withPoints: true, + // withTime: true, + // }); + // routes.map((route) => console.log(route.route.spacetimePoints)); const match = new Match(); match.uuid = 'e23f9725-2c19-49a0-9ef6-17d8b9a5ec85'; - // this._messager.publish('matcher.match', 'match !'); + this._messager.publish('matcher.match', 'match !'); return { data: [match], total: 1, }; } catch (error) { - // this._messager.publish( - // 'logging.matcher.match.crit', - // JSON.stringify({ - // matchQuery, - // error, - // }), - // ); + this._messager.publish( + 'logging.matcher.match.crit', + JSON.stringify({ + matchQuery, + error, + }), + ); throw error; } } diff --git a/src/modules/matcher/matcher.module.ts b/src/modules/matcher/matcher.module.ts index 7854892..7173746 100644 --- a/src/modules/matcher/matcher.module.ts +++ b/src/modules/matcher/matcher.module.ts @@ -14,6 +14,7 @@ import { redisStore } from 'cache-manager-ioredis-yet'; import { DefaultParamsProvider } from './adapters/secondaries/default-params.provider'; import { GeorouterCreator } from './adapters/secondaries/georouter-creator'; import { HttpModule } from '@nestjs/axios'; +import { MatcherGeodesic } from './adapters/secondaries/geodesic'; @Module({ imports: [ @@ -55,6 +56,7 @@ import { HttpModule } from '@nestjs/axios'; DefaultParamsProvider, MatchUseCase, GeorouterCreator, + MatcherGeodesic, ], exports: [], }) diff --git a/src/modules/matcher/tests/unit/default-params.provider.spec.ts b/src/modules/matcher/tests/unit/adapters/secondaries/default-params.provider.spec.ts similarity index 84% rename from src/modules/matcher/tests/unit/default-params.provider.spec.ts rename to src/modules/matcher/tests/unit/adapters/secondaries/default-params.provider.spec.ts index a721186..5221c14 100644 --- a/src/modules/matcher/tests/unit/default-params.provider.spec.ts +++ b/src/modules/matcher/tests/unit/adapters/secondaries/default-params.provider.spec.ts @@ -1,7 +1,7 @@ import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { DefaultParamsProvider } from '../../adapters/secondaries/default-params.provider'; -import { IDefaultParams } from '../../domain/types/default-params.type'; +import { DefaultParamsProvider } from '../../../../adapters/secondaries/default-params.provider'; +import { IDefaultParams } from '../../../../domain/types/default-params.type'; const mockConfigService = { get: jest.fn().mockImplementationOnce(() => 99), diff --git a/src/modules/matcher/tests/unit/adapters/secondaries/geodesic.spec.ts b/src/modules/matcher/tests/unit/adapters/secondaries/geodesic.spec.ts new file mode 100644 index 0000000..9e08335 --- /dev/null +++ b/src/modules/matcher/tests/unit/adapters/secondaries/geodesic.spec.ts @@ -0,0 +1,14 @@ +import { MatcherGeodesic } from '../../../../adapters/secondaries/geodesic'; + +describe('Matcher geodesic', () => { + it('should be defined', () => { + const geodesic: MatcherGeodesic = new MatcherGeodesic(); + expect(geodesic).toBeDefined(); + }); + it('should get inverse values', () => { + const geodesic: MatcherGeodesic = new MatcherGeodesic(); + const inv = geodesic.inverse(0, 0, 1, 1); + expect(Math.round(inv.azimuth)).toBe(45); + expect(Math.round(inv.distance)).toBe(156900); + }); +}); diff --git a/src/modules/matcher/tests/unit/georouter-creator.spec.ts b/src/modules/matcher/tests/unit/adapters/secondaries/georouter-creator.spec.ts similarity index 72% rename from src/modules/matcher/tests/unit/georouter-creator.spec.ts rename to src/modules/matcher/tests/unit/adapters/secondaries/georouter-creator.spec.ts index b7a7d6d..543991b 100644 --- a/src/modules/matcher/tests/unit/georouter-creator.spec.ts +++ b/src/modules/matcher/tests/unit/adapters/secondaries/georouter-creator.spec.ts @@ -1,9 +1,11 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { GeorouterCreator } from '../../adapters/secondaries/georouter-creator'; -import { GraphhopperGeorouter } from '../../adapters/secondaries/graphhopper-georouter'; +import { GeorouterCreator } from '../../../../adapters/secondaries/georouter-creator'; +import { GraphhopperGeorouter } from '../../../../adapters/secondaries/graphhopper-georouter'; import { HttpService } from '@nestjs/axios'; +import { MatcherGeodesic } from '../../../../adapters/secondaries/geodesic'; const mockHttpService = jest.fn(); +const mockMatcherGeodesic = jest.fn(); describe('Georouter creator', () => { let georouterCreator: GeorouterCreator; @@ -17,6 +19,10 @@ describe('Georouter creator', () => { provide: HttpService, useValue: mockHttpService, }, + { + provide: MatcherGeodesic, + useValue: mockMatcherGeodesic, + }, ], }).compile(); diff --git a/src/modules/matcher/tests/unit/adapters/secondaries/graphhopper-georouter.spec.ts b/src/modules/matcher/tests/unit/adapters/secondaries/graphhopper-georouter.spec.ts new file mode 100644 index 0000000..71dd199 --- /dev/null +++ b/src/modules/matcher/tests/unit/adapters/secondaries/graphhopper-georouter.spec.ts @@ -0,0 +1,268 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HttpService } from '@nestjs/axios'; +import { GeorouterCreator } from '../../../../adapters/secondaries/georouter-creator'; +import { IGeorouter } from '../../../../domain/interfaces/georouter.interface'; +import { of } from 'rxjs'; +import { AxiosError } from 'axios'; +import { MatcherGeodesic } from '../../../../adapters/secondaries/geodesic'; + +const mockHttpService = { + get: jest + .fn() + .mockImplementationOnce(() => { + throw new AxiosError('Axios error !'); + }) + .mockImplementationOnce(() => { + return of({ + status: 200, + data: { + paths: [ + { + distance: 50000, + time: 1800000, + snapped_waypoints: { + coordinates: [ + [0, 0], + [10, 10], + ], + }, + }, + ], + }, + }); + }) + .mockImplementationOnce(() => { + return of({ + status: 200, + data: { + paths: [ + { + distance: 50000, + time: 1800000, + points: { + coordinates: [ + [0, 0], + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [5, 5], + [6, 6], + [7, 7], + [8, 8], + [9, 9], + [10, 10], + ], + }, + snapped_waypoints: { + coordinates: [ + [0, 0], + [10, 10], + ], + }, + }, + ], + }, + }); + }) + .mockImplementationOnce(() => { + return of({ + status: 200, + data: { + paths: [ + { + distance: 50000, + time: 1800000, + points: { + coordinates: [ + [0, 0], + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [5, 5], + [6, 6], + [7, 7], + [8, 8], + [9, 9], + [10, 10], + ], + }, + details: { + time: [ + [0, 1, 180000], + [1, 2, 180000], + [2, 3, 180000], + [3, 4, 180000], + [4, 5, 180000], + [5, 6, 180000], + [6, 7, 180000], + [7, 9, 360000], + [9, 10, 180000], + ], + }, + snapped_waypoints: { + coordinates: [ + [0, 0], + [10, 10], + ], + }, + }, + ], + }, + }); + }), +}; + +const mockMatcherGeodesic = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + inverse: jest.fn().mockImplementation(() => ({ + azimuth: 45, + distance: 50000, + })), +}; + +describe('Graphhopper Georouter', () => { + let georouterCreator: GeorouterCreator; + let graphhopperGeorouter: IGeorouter; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + GeorouterCreator, + { + provide: HttpService, + useValue: mockHttpService, + }, + { + provide: MatcherGeodesic, + useValue: mockMatcherGeodesic, + }, + ], + }).compile(); + + georouterCreator = module.get(GeorouterCreator); + graphhopperGeorouter = georouterCreator.create( + 'graphhopper', + 'http://localhost', + ); + }); + + it('should be defined', () => { + expect(graphhopperGeorouter).toBeDefined(); + }); + + describe('route function', () => { + it('should fail on axios error', async () => { + await expect( + graphhopperGeorouter.route( + [ + { + key: 'route1', + points: [ + { + lat: 0, + lon: 0, + }, + { + lat: 1, + lon: 1, + }, + ], + }, + ], + { + withDistance: false, + withPoints: false, + withTime: false, + }, + ), + ).rejects.toBeInstanceOf(Error); + }); + it('should create one route with all settings to false', async () => { + const routes = await graphhopperGeorouter.route( + [ + { + key: 'route1', + points: [ + { + lat: 0, + lon: 0, + }, + { + lat: 10, + lon: 10, + }, + ], + }, + ], + { + withDistance: false, + withPoints: false, + withTime: false, + }, + ); + expect(routes).toHaveLength(1); + expect(routes[0].route.distance).toBe(50000); + }); + it('should create one route with points', async () => { + const routes = await graphhopperGeorouter.route( + [ + { + key: 'route1', + points: [ + { + lat: 0, + lon: 0, + }, + { + lat: 10, + lon: 10, + }, + ], + }, + ], + { + withDistance: false, + withPoints: true, + withTime: false, + }, + ); + expect(routes).toHaveLength(1); + expect(routes[0].route.distance).toBe(50000); + expect(routes[0].route.duration).toBe(1800); + expect(routes[0].route.fwdAzimuth).toBe(45); + expect(routes[0].route.backAzimuth).toBe(225); + expect(routes[0].route.points.length).toBe(11); + }); + it('should create one route with points and time', async () => { + const routes = await graphhopperGeorouter.route( + [ + { + key: 'route1', + points: [ + { + lat: 0, + lon: 0, + }, + { + lat: 10, + lon: 10, + }, + ], + }, + ], + { + withDistance: false, + withPoints: true, + withTime: true, + }, + ); + expect(routes).toHaveLength(1); + expect(routes[0].route.spacetimePoints.length).toBe(2); + expect(routes[0].route.spacetimePoints[1].duration).toBe(1800); + expect(routes[0].route.spacetimePoints[1].distance).toBeUndefined(); + }); + }); +}); diff --git a/src/modules/matcher/tests/unit/messager.spec.ts b/src/modules/matcher/tests/unit/adapters/secondaries/messager.spec.ts similarity index 94% rename from src/modules/matcher/tests/unit/messager.spec.ts rename to src/modules/matcher/tests/unit/adapters/secondaries/messager.spec.ts index 0331332..0bd23a9 100644 --- a/src/modules/matcher/tests/unit/messager.spec.ts +++ b/src/modules/matcher/tests/unit/adapters/secondaries/messager.spec.ts @@ -1,7 +1,7 @@ import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { Messager } from '../../adapters/secondaries/messager'; +import { Messager } from '../../../../adapters/secondaries/messager'; const mockAmqpConnection = { publish: jest.fn().mockImplementation(), diff --git a/src/modules/matcher/tests/unit/geography.spec.ts b/src/modules/matcher/tests/unit/domain/geography.spec.ts similarity index 97% rename from src/modules/matcher/tests/unit/geography.spec.ts rename to src/modules/matcher/tests/unit/domain/geography.spec.ts index 7c52c8c..e10b773 100644 --- a/src/modules/matcher/tests/unit/geography.spec.ts +++ b/src/modules/matcher/tests/unit/domain/geography.spec.ts @@ -1,4 +1,4 @@ -import { Geography } from '../../domain/entities/geography'; +import { Geography } from '../../../domain/entities/geography'; describe('Geography entity', () => { it('should be defined', () => { diff --git a/src/modules/matcher/tests/unit/match.usecase.spec.ts b/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts similarity index 80% rename from src/modules/matcher/tests/unit/match.usecase.spec.ts rename to src/modules/matcher/tests/unit/domain/match.usecase.spec.ts index dace03d..6de7ad9 100644 --- a/src/modules/matcher/tests/unit/match.usecase.spec.ts +++ b/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts @@ -1,13 +1,13 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { Messager } from '../../adapters/secondaries/messager'; -import { MatchUseCase } from '../../domain/usecases/match.usecase'; -import { MatchRequest } from '../../domain/dtos/match.request'; -import { MatchQuery } from '../../queries/match.query'; -import { AdRepository } from '../../adapters/secondaries/ad.repository'; +import { Messager } from '../../../adapters/secondaries/messager'; +import { MatchUseCase } from '../../../domain/usecases/match.usecase'; +import { MatchRequest } from '../../../domain/dtos/match.request'; +import { MatchQuery } from '../../../queries/match.query'; +import { AdRepository } from '../../../adapters/secondaries/ad.repository'; import { AutomapperModule } from '@automapper/nestjs'; import { classes } from '@automapper/classes'; -import { IDefaultParams } from '../../domain/types/default-params.type'; -import { Algorithm } from '../../domain/types/algorithm.enum'; +import { IDefaultParams } from '../../../domain/types/default-params.type'; +import { Algorithm } from '../../../domain/types/algorithm.enum'; const mockAdRepository = {}; diff --git a/src/modules/matcher/tests/unit/person.spec.ts b/src/modules/matcher/tests/unit/domain/person.spec.ts similarity index 94% rename from src/modules/matcher/tests/unit/person.spec.ts rename to src/modules/matcher/tests/unit/domain/person.spec.ts index 2ff144f..56c2e47 100644 --- a/src/modules/matcher/tests/unit/person.spec.ts +++ b/src/modules/matcher/tests/unit/domain/person.spec.ts @@ -1,4 +1,4 @@ -import { Person } from '../../domain/entities/person'; +import { Person } from '../../../domain/entities/person'; const DEFAULT_IDENTIFIER = 0; const MARGIN_DURATION = 900; diff --git a/src/modules/matcher/tests/unit/domain/route.spec.ts b/src/modules/matcher/tests/unit/domain/route.spec.ts new file mode 100644 index 0000000..d281452 --- /dev/null +++ b/src/modules/matcher/tests/unit/domain/route.spec.ts @@ -0,0 +1,55 @@ +import { Route } from '../../../domain/entities/route'; +import { SpacetimePoint } from '../../../domain/entities/spacetime-point'; +import { Waypoint } from '../../../domain/entities/waypoint'; + +const mockGeodesic = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + inverse: jest.fn().mockImplementation((lon1, lat1, lon2, lat2) => { + return lon1 == 0 + ? { + azimuth: 45, + distance: 50000, + } + : { + azimuth: -45, + distance: 60000, + }; + }), +}; + +describe('Route entity', () => { + it('should be defined', () => { + const route = new Route(mockGeodesic); + expect(route).toBeDefined(); + }); + it('should set waypoints and geodesic values for a route', () => { + const route = new Route(mockGeodesic); + const waypoint1: Waypoint = new Waypoint(); + waypoint1.point = [0, 0]; + const waypoint2: Waypoint = new Waypoint(); + waypoint2.point = [10, 10]; + route.setWaypoints([waypoint1, waypoint2]); + expect(route.waypoints.length).toBe(2); + expect(route.fwdAzimuth).toBe(45); + expect(route.backAzimuth).toBe(225); + expect(route.distanceAzimuth).toBe(50000); + }); + it('should set points and geodesic values for a route', () => { + const route = new Route(mockGeodesic); + route.setPoints([ + [10, 10], + [20, 20], + ]); + expect(route.points.length).toBe(2); + expect(route.fwdAzimuth).toBe(315); + expect(route.backAzimuth).toBe(135); + expect(route.distanceAzimuth).toBe(60000); + }); + it('should set spacetimePoints for a route', () => { + const route = new Route(mockGeodesic); + const spacetimePoint1 = new SpacetimePoint([0, 0], 0, 0); + const spacetimePoint2 = new SpacetimePoint([10, 10], 500, 5000); + route.setSpacetimePoints([spacetimePoint1, spacetimePoint2]); + expect(route.spacetimePoints.length).toBe(2); + }); +}); diff --git a/src/modules/matcher/tests/unit/time.spec.ts b/src/modules/matcher/tests/unit/domain/time.spec.ts similarity index 98% rename from src/modules/matcher/tests/unit/time.spec.ts rename to src/modules/matcher/tests/unit/domain/time.spec.ts index 0d8cbdd..5cc3929 100644 --- a/src/modules/matcher/tests/unit/time.spec.ts +++ b/src/modules/matcher/tests/unit/domain/time.spec.ts @@ -1,4 +1,4 @@ -import { Time } from '../../domain/entities/time'; +import { Time } from '../../../domain/entities/time'; const MARGIN_DURATION = 900; const VALIDITY_DURATION = 365; diff --git a/src/modules/matcher/tests/unit/graphhopper-georouter.spec.ts b/src/modules/matcher/tests/unit/graphhopper-georouter.spec.ts deleted file mode 100644 index 64cc698..0000000 --- a/src/modules/matcher/tests/unit/graphhopper-georouter.spec.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { HttpService } from '@nestjs/axios'; -import { NamedRoute } from '../../domain/entities/named-route'; -import { GeorouterCreator } from '../../adapters/secondaries/georouter-creator'; -import { IGeorouter } from '../../domain/interfaces/georouter.interface'; -import { of } from 'rxjs'; -import { AxiosError } from 'axios'; - -const mockHttpService = { - get: jest - .fn() - .mockImplementationOnce(() => { - throw new AxiosError('Axios error !'); - }) - .mockImplementation(() => { - return of({ - status: 200, - data: [new NamedRoute()], - }); - }), -}; - -describe('Graphhopper Georouter', () => { - let georouterCreator: GeorouterCreator; - let graphhopperGeorouter: IGeorouter; - - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [], - providers: [ - GeorouterCreator, - { - provide: HttpService, - useValue: mockHttpService, - }, - ], - }).compile(); - - georouterCreator = module.get(GeorouterCreator); - graphhopperGeorouter = georouterCreator.create( - 'graphhopper', - 'http://localhost', - ); - }); - - it('should be defined', () => { - expect(graphhopperGeorouter).toBeDefined(); - }); - - describe('route function', () => { - it('should fail on axios error', async () => { - await expect( - graphhopperGeorouter.route( - [ - { - key: 'route1', - points: [ - { - lat: 49.440041, - lon: 1.093912, - }, - { - lat: 50.630992, - lon: 3.045432, - }, - ], - }, - ], - { - withDistance: false, - withPoints: false, - withTime: false, - }, - ), - ).rejects.toBeInstanceOf(Error); - }); - it('should create one route with all settings to false', async () => { - const routes = await graphhopperGeorouter.route( - [ - { - key: 'route1', - points: [ - { - lat: 49.440041, - lon: 1.093912, - }, - { - lat: 50.630992, - lon: 3.045432, - }, - ], - }, - ], - { - withDistance: false, - withPoints: false, - withTime: false, - }, - ); - expect(routes).toHaveLength(1); - }); - it('should create 2 routes with distance, points and time', async () => { - const routes = await graphhopperGeorouter.route( - [ - { - key: 'route1', - points: [ - { - lat: 49.440041, - lon: 1.093912, - }, - { - lat: 50.630992, - lon: 3.045432, - }, - ], - }, - { - key: 'route2', - points: [ - { - lat: 49.440041, - lon: 1.093912, - }, - { - lat: 50.630992, - lon: 3.045432, - }, - ], - }, - ], - { - withDistance: true, - withPoints: true, - withTime: true, - }, - ); - expect(routes).toHaveLength(2); - }); - }); -}); diff --git a/src/modules/matcher/tests/unit/match.query.spec.ts b/src/modules/matcher/tests/unit/queries/match.query.spec.ts similarity index 93% rename from src/modules/matcher/tests/unit/match.query.spec.ts rename to src/modules/matcher/tests/unit/queries/match.query.spec.ts index 6e9182f..8ed650b 100644 --- a/src/modules/matcher/tests/unit/match.query.spec.ts +++ b/src/modules/matcher/tests/unit/queries/match.query.spec.ts @@ -1,9 +1,9 @@ -import { MatchRequest } from '../../domain/dtos/match.request'; -import { Role } from '../../domain/types/role.enum'; -import { TimingFrequency } from '../../domain/types/timing'; -import { IDefaultParams } from '../../domain/types/default-params.type'; -import { MatchQuery } from '../../queries/match.query'; -import { Algorithm } from '../../domain/types/algorithm.enum'; +import { MatchRequest } from '../../../domain/dtos/match.request'; +import { Role } from '../../../domain/types/role.enum'; +import { TimingFrequency } from '../../../domain/types/timing'; +import { IDefaultParams } from '../../../domain/types/default-params.type'; +import { MatchQuery } from '../../../queries/match.query'; +import { Algorithm } from '../../../domain/types/algorithm.enum'; const defaultParams: IDefaultParams = { DEFAULT_IDENTIFIER: 0,