import { Controller, Get, Post, Delete, Body, Query, UseGuards, HttpException, HttpStatus } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiQuery } from '@nestjs/swagger'; import { GeocodingService } from './geocoding.service'; import { ApiKeyGuard } from '../common/guards/api-key.guard'; @ApiTags('geocoding') @Controller('geocoding') @UseGuards(ApiKeyGuard) export class GeocodingController { constructor(private readonly geocodingService: GeocodingService) {} @Get('search') @ApiOperation({ summary: 'Search for locations (Forward Geocoding)' }) @ApiQuery({ name: 'q', required: true }) @ApiQuery({ name: 'lat', required: false, type: Number }) @ApiQuery({ name: 'lng', required: false, type: Number }) @ApiQuery({ name: 'radius', required: false, type: Number }) @ApiQuery({ name: 'country', required: false, enum: ['jordan', 'syria', 'egypt'] }) async search( @Query('q') query: string, @Query('lat') lat?: string, @Query('lng') lng?: string, @Query('radius') radius?: string, @Query('country') country?: string, ) { // NestJS @Query() always receives strings — must parse explicitly for numeric types const parsedLat = lat !== undefined ? parseFloat(lat) : undefined; const parsedLng = lng !== undefined ? parseFloat(lng) : undefined; const parsedRadius = radius !== undefined ? parseFloat(radius) : 20000; // Guard against malformed float values (NaN breaks PostGIS) const safeLat = parsedLat !== undefined && !isNaN(parsedLat) ? parsedLat : undefined; const safeLng = parsedLng !== undefined && !isNaN(parsedLng) ? parsedLng : undefined; return this.geocodingService.searchPlaces(query, safeLat, safeLng, parsedRadius, country); } @Get('reverse') @ApiOperation({ summary: 'Reverse Geocoding (Lat/Lng to Address)' }) @ApiQuery({ name: 'lat', required: true }) @ApiQuery({ name: 'lng', required: true }) async reverse( @Query('lat') lat: string, @Query('lng') lng: string, ) { const parsedLat = parseFloat(lat); const parsedLng = parseFloat(lng); if (isNaN(parsedLat) || isNaN(parsedLng)) { throw new HttpException('Invalid lat/lng values', HttpStatus.BAD_REQUEST); } return this.geocodingService.reverseGeocode(parsedLat, parsedLng); } @Post('places') @ApiOperation({ summary: 'Add a new location (User Submitted)' }) async addPlace(@Body() placeData: any) { return this.geocodingService.addPlace(placeData); } @Delete('places') @ApiOperation({ summary: 'Delete a place by name or ID' }) @ApiQuery({ name: 'name', required: false }) @ApiQuery({ name: 'id', required: false, type: Number }) @ApiQuery({ name: 'country', required: true, enum: ['jordan', 'syria', 'egypt'] }) async deletePlace( @Query('country') country: string, @Query('name') name?: string, @Query('id') id?: string, ) { if (id) { return this.geocodingService.deletePlaceById(Number(id), country); } if (name) { return this.geocodingService.deletePlacesByName(name, country); } throw new HttpException('Name or ID required', HttpStatus.BAD_REQUEST); } @Post('upsert-place') @ApiOperation({ summary: 'Add or Update a location (Automated Scraper)' }) async upsertPlace(@Body() placeData: any) { return this.geocodingService.upsertPlace(placeData); } @Post('upsert-batch') @ApiOperation({ summary: 'Add or Update multiple locations in bulk' }) async upsertBatch(@Body() body: { places: any[] }) { return this.geocodingService.upsertBatch(body.places); } @Get('places') @ApiOperation({ summary: 'Get recent user submitted places' }) async getPlaces(@Query('limit') limit?: string) { return this.geocodingService.getRecentPlaces(limit ? parseInt(limit, 10) : 50); } @Get('geojson') @ApiOperation({ summary: 'Get all user submitted places as GeoJSON for Map Style' }) async getGeoJSON() { return this.geocodingService.getAllPlacesGeoJSON(); } }