import { Controller, Get, Post, Delete, Body, Query, UseGuards, HttpException, HttpStatus } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiQuery, ApiHeader } from '@nestjs/swagger'; import { GeocodingService } from './geocoding.service'; import { AdminBoundariesService } from './admin-boundaries.service'; import { JordanResearchService } from './jordan-research.service'; import { AdministrativeLinkingService } from './administrative-linking.service'; import { ApiKeyGuard } from '../common/guards/api-key.guard'; import { TenantThrottlerGuard } from '../common/guards/rate-limiter.guard'; import { SearchQueryDto } from './dto/search-query.dto'; import { ReverseGeocodeDto } from './dto/reverse-geocode.dto'; @ApiTags('geocoding') @ApiHeader({ name: 'x-api-key', description: 'Multi-tenant API Key', required: true }) @Controller('geocoding') @UseGuards(ApiKeyGuard, TenantThrottlerGuard) export class GeocodingController { constructor( private readonly geocodingService: GeocodingService, private readonly adminBoundariesService: AdminBoundariesService, private readonly jordanResearchService: JordanResearchService, private readonly adminLinkingService: AdministrativeLinkingService, ) {} @Get('search') @ApiOperation({ summary: 'Search for locations (Forward Geocoding)' }) async search(@Query() queryDto: SearchQueryDto) { const { q, lat, lng, radius, country } = queryDto; // ValidationPipe with { transform: true } handles numeric conversion and defaults return this.geocodingService.searchPlaces(q, lat, lng, radius, country); } @Get('reverse') @ApiOperation({ summary: 'Reverse Geocoding (Lat/Lng to Address)' }) async reverse(@Query() reverseDto: ReverseGeocodeDto) { const { lat, lng } = reverseDto; return this.geocodingService.reverseGeocode(lat, lng); } @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(); } @Post('import-boundaries') @ApiOperation({ summary: 'Import administrative boundaries from a local GeoJSON file on the server' }) @ApiQuery({ name: 'country', required: true }) @ApiQuery({ name: 'filePath', required: true }) async importBoundaries( @Query('country') country: string, @Query('filePath') filePath: string, ) { return this.adminBoundariesService.importFromFile(country, filePath); } @Get('research/zarqa') @ApiOperation({ summary: 'Generate a research report for Zarqa, Jordan (Sample Data)' }) async zarqaResearch() { return this.jordanResearchService.generateZarqaReport(); } @Post('admin/sync-neighborhoods') @ApiOperation({ summary: 'Sync neighborhood points from OSM for a bbox' }) @ApiQuery({ name: 'bbox', required: false }) @ApiQuery({ name: 'country', required: false, enum: ['jordan', 'syria', 'egypt'] }) async syncNeighborhoods(@Query('bbox') bbox?: string, @Query('country') country?: string) { return this.adminLinkingService.syncOsmNeighborhoodPoints(bbox, country); } @Post('admin/generate-voronoi') @ApiOperation({ summary: 'Generate Voronoi polygons for neighborhoods' }) @ApiQuery({ name: 'country', required: false, enum: ['jordan', 'syria', 'egypt'] }) async generateVoronoi(@Query('country') country?: string) { return this.adminLinkingService.generateVoronoiNeighborhoods(country); } @Post('admin/link-places') @ApiOperation({ summary: 'Link places to administrative hierarchy' }) @ApiQuery({ name: 'country', required: true, enum: ['jordan', 'syria', 'egypt'] }) async linkPlaces(@Query('country') country: 'jordan' | 'syria' | 'egypt') { return this.adminLinkingService.linkPlaces(country); } }