128 lines
5.1 KiB
TypeScript
128 lines
5.1 KiB
TypeScript
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);
|
|
}
|
|
}
|