Files
maps-saas/apps/api/src/maps/maps.controller.ts
T
2026-04-16 03:39:49 +03:00

121 lines
4.0 KiB
TypeScript

import { Controller, Get, Query, UseGuards, Res } from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
import type { Response } from 'express';
import * as fs from 'fs';
import * as path from 'path';
import { MapsService } from './maps.service';
import { ApiKeyGuard } from '../common/guards/api-key.guard';
@ApiTags('maps')
@Controller('maps')
@UseGuards(ApiKeyGuard)
export class MapsController {
constructor(private readonly mapsService: MapsService) { }
@Get('style.json')
@ApiOperation({ summary: 'Get MapLibre style JSON 🎨' })
async getStyleJson(@Query('theme') theme: string, @Res() res: Response) {
// Determine filenames based on theme
const isDark = theme === 'obsidian';
const filename = isDark ? 'style-dark.json' : 'style.json';
const fallbackFilename = 'style.json';
// Paths to check
const pathsToCheck = [
path.join('/data', filename),
path.join(process.cwd(), '../../', filename),
path.join(process.cwd(), filename),
// Fallbacks to light style if dark is missing
path.join('/data', fallbackFilename),
path.join(process.cwd(), '../../', fallbackFilename),
path.join(process.cwd(), fallbackFilename),
];
let stylePath = '';
for (const p of pathsToCheck) {
if (fs.existsSync(p)) {
stylePath = p;
break;
}
}
if (!stylePath) {
return res.status(404).send('Style not found');
}
try {
const styleRaw = fs.readFileSync(stylePath, 'utf8');
const styleObj = JSON.parse(styleRaw);
// Dynamic Theme support (Safety overrides or fine-tuning)
if (theme === 'light') {
styleObj.layers.forEach((layer: any) => {
if (layer.id === 'background') {
layer.paint['background-color'] = '#FFFFFF';
}
});
} else if (theme === 'obsidian') {
// If we found style-dark.json, we don't strictly need this,
// but keeping it as a helper or if it fell back to style.json
styleObj.layers.forEach((layer: any) => {
if (layer.id === 'background') {
layer.paint['background-color'] = '#101014'; // Dark tone
}
});
}
res.setHeader('Content-Type', 'application/json');
res.send(styleObj);
} catch (e) {
res.status(500).send('Error parsing style.json');
}
}
@Get('route')
@ApiOperation({ summary: 'Calculate a route with dynamic waypoints 🚗' })
async getRoute(@Query() query: any) {
const waypoints: [number, number][] = [];
// 1. Extract Origin (fromLat, fromLng)
if (query.fromLat && query.fromLng) {
waypoints.push([parseFloat(query.fromLat), parseFloat(query.fromLng)]);
}
// 2. Extract Intermediate Stops (stop1Lat, stop1Lng, stop2Lat, etc.)
// We sort keys to ensure stops are in order: stop1, stop2, stop3...
const stopKeys = Object.keys(query)
.filter(key => key.startsWith('stop') && key.endsWith('Lat'))
.sort((a, b) => {
const numA = parseInt(a.replace('stop', '').replace('Lat', ''), 10);
const numB = parseInt(b.replace('stop', '').replace('Lat', ''), 10);
return numA - numB;
});
for (const latKey of stopKeys) {
const prefix = latKey.replace('Lat', '');
const lngKey = `${prefix}Lng`;
if (query[latKey] && query[lngKey]) {
waypoints.push([parseFloat(query[latKey]), parseFloat(query[lngKey])]);
}
}
// 3. Extract Destination (toLat, toLng)
if (query.toLat && query.toLng) {
waypoints.push([parseFloat(query.toLat), parseFloat(query.toLng)]);
}
const profile = query.profile || 'car';
const steps = query.steps === 'true';
const locale = query.locale || 'en';
const alternatives = query.alternatives === 'true';
return this.mapsService.getRoute(waypoints, profile, steps, locale, alternatives);
}
@Get('config')
@ApiOperation({ summary: 'Get map configuration for the region 🗺️' })
async getConfig() {
return this.mapsService.getMapConfig();
}
}