All files / src/routes geocode.ts

100% Statements 64/64
60% Branches 6/10
100% Functions 2/2
100% Lines 64/64

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 851x           1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x   17x 17x 17x 17x 17x 17x 17x 17x 17x   17x 1x 1x   16x 16x   1x 39x     39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 6x 6x   6x             6x 6x 6x 6x   6x 6x 6x 6x   3x 6x 39x 39x  
import { AppError } from "@ontrack/backend-common";
import type { FastifyPluginAsync } from "fastify";
import type { AppConfig } from "../config.js";
import { resolveTrackGeocode } from "../lib/route-layout.js";
import { assertServiceToken } from "../lib/service-token.js";
 
const reverseResponseSchema = {
  type: "object",
  required: ["locality", "routeLayoutCode", "startLocality", "endLocality"],
  additionalProperties: false,
  properties: {
    locality: { type: "string" },
    routeLayoutCode: { type: "string", enum: ["CIRCULAR", "LINEAR"] },
    startLocality: { type: "string" },
    endLocality: { type: "string" },
  },
} as const;
 
function parseCoordinate(
  value: unknown,
  field: "lat" | "lon",
  min: number,
  max: number,
): number {
  const raw = Array.isArray(value) ? value[0] : value;
  const parsed =
    typeof raw === "string" || typeof raw === "number" ? Number.parseFloat(String(raw)) : Number.NaN;
 
  if (!Number.isFinite(parsed) || parsed < min || parsed > max) {
    throw new AppError(400, "INVALID_COORDINATES", `Invalid ${field}`);
  }
 
  return parsed;
}
 
export const geocodeRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (app, opts) => {
  const { config } = opts;
 
  /** Reverse geocode старта и финиша GPX + тип маршрута (круговой/линейный). */
  app.get(
    "/geocode/reverse",
    {
      schema: {
        querystring: {
          type: "object",
          required: ["startLat", "startLon", "endLat", "endLon"],
          additionalProperties: false,
          properties: {
            startLat: { type: "string" },
            startLon: { type: "string" },
            endLat: { type: "string" },
            endLon: { type: "string" },
          },
        },
        response: {
          200: reverseResponseSchema,
        },
      },
    },
    async (request, reply) => {
      const token = request.headers["x-service-token"];
      assertServiceToken(Array.isArray(token) ? token[0] : token, config.serviceToken);
 
      const query = request.query as {
        startLat?: string;
        startLon?: string;
        endLat?: string;
        endLon?: string;
      };
 
      const startLat = parseCoordinate(query.startLat, "lat", -90, 90);
      const startLon = parseCoordinate(query.startLon, "lon", -180, 180);
      const endLat = parseCoordinate(query.endLat, "lat", -90, 90);
      const endLon = parseCoordinate(query.endLon, "lon", -180, 180);
 
      const result = await resolveTrackGeocode(startLat, startLon, endLat, endLon, {
        baseUrl: config.nominatimBaseUrl,
        userAgent: config.nominatimUserAgent,
      });
 
      return reply.send(result);
    },
  );
};