Skip to content

Commit

Permalink
added lat,lng,radius to places/categories request
Browse files Browse the repository at this point in the history
  • Loading branch information
AdenForshaw committed Nov 12, 2024
1 parent 379f227 commit 6ab2688
Show file tree
Hide file tree
Showing 10 changed files with 148 additions and 27 deletions.
17 changes: 17 additions & 0 deletions extra-datasets/cities/demo-cities.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[{
"city": "New York",
"lat": 40.7128,
"lng": -74.0060
},{
"city":"London",
"lat": 51.5074,
"lng": -0.1278
},{
"city":"Paris",
"lat": 48.8566,
"lng": 2.3522
},{
"city":"Bondi Beach",
"lat": -33.8910,
"lng": 151.2769
}]
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "overture-maps-api",
"version": "0.1.2",
"version": "0.1.3",
"description": "",
"author": "",
"private": true,
Expand Down
92 changes: 80 additions & 12 deletions src/bigquery/bigquery.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class BigQueryService {

// New method to get brands based on country or lat/lng/radius
async getBrandsNearby(
country_code?: string,
country?: string,
latitude?: number,
longitude?: number,
radius: number = 1000,
Expand All @@ -51,8 +51,14 @@ export class BigQueryService {
FROM \`${SOURCE_DATASET}.place\`
`;

if (country_code) {
query += ` WHERE addresses.list[OFFSET(0)].element.country = "${country_code}"`;
const whereClauses = this.buildWhereClauses({ country, latitude, longitude, radius, categories, require_wikidata });
if (whereClauses.length > 0) {
query += ` WHERE ${whereClauses.join(' AND ')}`;
}

/*
if (country) {
query += ` WHERE addresses.list[OFFSET(0)].element.country = "${country}"`;
} else if (latitude && longitude) {
query += ` WHERE ST_DISTANCE(
ST_GEOGPOINT(${longitude}, ${latitude}),
Expand All @@ -65,7 +71,7 @@ export class BigQueryService {
}
if (require_wikidata) {
query += ` AND brand.wikidata IS NOT NULL`;
}
}*/
query += ` GROUP BY ALL`;
if (minimum_places) {
query += ` HAVING count_places >= ${minimum_places}`;
Expand Down Expand Up @@ -102,18 +108,27 @@ export class BigQueryService {
}));
}

async getCategories(country?:string): Promise<{ primary: string; counts:{ places:number } }[]> {
async getCategories(
country?:string,
latitude?: number,
longitude?: number,
radius: number = 1000
): Promise<{ primary: string; counts:{ places:number } }[]> {



let query = `-- Overture Maps API: Get categories
SELECT DISTINCT categories.primary AS category_primary,
count(1) as count_places,
count(distinct brand.names.primary) as count_brands
FROM \`${SOURCE_DATASET}.place\`
WHERE categories.primary IS NOT NULL
`;
if (country) {
query += ` AND addresses.list[OFFSET(0)].element.country = "${country}"`

const whereClauses = this.buildWhereClauses({ country, latitude, longitude, radius });
if (whereClauses.length > 0) {
query += ` AND ${whereClauses.join(' AND ')}`;
}

query += ` GROUP BY category_primary
ORDER BY count_places DESC;
`;
Expand All @@ -127,7 +142,10 @@ export class BigQueryService {
brands: row.count_brands
}
}));
}async getPlacesWithNearestBuilding(
}


async getPlacesWithNearestBuilding(
latitude: number,
longitude: number,
radius: number = 1000,
Expand All @@ -153,9 +171,10 @@ export class BigQueryService {
// Build the WHERE clause for additional filters
let whereClauses: string[] = [];

if (latitude && longitude && radius) {
// Add radius condition if not already in WHERE clause
whereClauses.push(`ST_DWithin(p.geometry, ST_GeogPoint(${longitude}, ${latitude}), ${radius})`);

whereClauses.push(`ST_DWithin(p.geometry, ST_GeogPoint(${longitude}, ${latitude}), ${radius})`);
}
if (brand_wikidata) {
whereClauses.push(`p.brand.wikidata = "${brand_wikidata}"`);
}
Expand All @@ -165,7 +184,7 @@ export class BigQueryService {
}

if (country) {
whereClauses.push(`p.addresses[OFFSET(0)].country = "${country}"`);
whereClauses.push(`p.addresses[OFFSET(0)].element.country = "${country}"`);
}

if (categories && categories.length > 0) {
Expand Down Expand Up @@ -432,4 +451,53 @@ WHERE ST_WITHIN(s.geometry, search_area_geometry) and ST_DWithin(geometry, ST_Ge

return {rows,statistics};
}

private buildWhereClauses({
country,
latitude,
longitude,
radius,
brand_wikidata,
brand_name,
categories,
min_confidence,
require_wikidata
}: {
country?: string;
latitude?: number;
longitude?: number;
radius?: number;
brand_wikidata?: string;
brand_name?: string;
categories?: string[];
min_confidence?: number;
require_wikidata?: boolean;
}): string[] {
const whereClauses: string[] = [];

if (latitude && longitude && radius) {
whereClauses.push(`ST_DWithin(geometry, ST_GeogPoint(${longitude}, ${latitude}), ${radius})`);
}
if (country) {
whereClauses.push(`addresses.list[OFFSET(0)].element.country = "${country}"`);
}
if (brand_wikidata) {
whereClauses.push(`brand.wikidata = "${brand_wikidata}"`);
}
if (brand_name) {
whereClauses.push(`brand.names.primary = "${brand_name}"`);
}
if (categories && categories.length > 0) {
whereClauses.push(`categories.primary IN UNNEST(["${categories.join('","')}"])`);
}
if (min_confidence !== undefined) {
whereClauses.push(`confidence >= ${min_confidence}`);
}
if (require_wikidata) {
whereClauses.push(`brand.wikidata IS NOT NULL`);
}

return whereClauses;
}

}
10 changes: 6 additions & 4 deletions src/common/dto/requests/get-by-location.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer';
import { IsArray, IsIn, IsNumber, IsOptional, IsString, Min, ValidateIf } from 'class-validator';
import { IsArray, IsIn, IsNumber, IsOptional, IsString, Max, Min, ValidateIf } from 'class-validator';

//format string enums
export enum Format {
Expand Down Expand Up @@ -43,16 +43,17 @@ export class GetByLocationDto {
radius?: number = 1000;

@ApiPropertyOptional({
description: 'Limit on the number of results returned, defaulting to 100 if not provided.',
description: 'Limit on the number of results returned, defaulting to 1000 if not provided.',
example: 10,
minimum: 1,
default: 100,
default: 1000,
})
@IsOptional()
@Transform(({ value }) => parseInt(value))
@IsNumber()
@Min(1)
limit?: number = 100;
@Max(25000, { message: 'Limit must be less than 25000, if you need a larger export then directly query the API otherwise the response will be too large' })
limit?: number = 25000;


@ApiPropertyOptional({
Expand All @@ -76,4 +77,5 @@ export class GetByLocationDto {
@Transform(({ value }) => String(value).split(','))
@IsString({ each: true })
includes?: string[];

}
6 changes: 6 additions & 0 deletions src/decorators/count-header.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { UseInterceptors, applyDecorators } from '@nestjs/common';
import { CountHeaderInterceptor } from '../interceptors/count-header.interceptor';

export function CountHeader() {
return applyDecorators(UseInterceptors(CountHeaderInterceptor));
}
2 changes: 1 addition & 1 deletion src/decorators/validate-lat-lng-user.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function ValidateLatLngUser(): MethodDecorator {
const lng = request?.lng;
const user = args[1]

if (lat && lng && user.isDemoAccount) {
if (lat && lng && user?.isDemoAccount) {
throw new ForbiddenException('Demo accounts cannot access this feature');
}

Expand Down
21 changes: 21 additions & 0 deletions src/interceptors/count-header.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Response } from 'express';

@Injectable()
export class CountHeaderInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const response = context.switchToHttp().getResponse<Response>();

return next.handle().pipe(
map((data) => {
if (Array.isArray(data)) {
console.log('applyHeader', data.length);
response.set('X-Total-Count', data.length.toString());
}
return data;
}),
);
}
}
3 changes: 2 additions & 1 deletion src/places/dto/requests/get-categories.dto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
import { GetByLocationDto } from 'src/common/dto/requests/get-by-location.dto';

export class GetCategoriesDto {
export class GetCategoriesDto extends GetByLocationDto {
@ApiPropertyOptional({
description: 'ISO 3166 country code consisting of 2 characters.',
example: 'US',
Expand Down
20 changes: 13 additions & 7 deletions src/places/places.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Format } from '../common/dto/requests/get-by-location.dto';
import { BuildingsService } from '../buildings/buildings.service';
import { Feature, GeoJsonObject, Geometry } from 'geojson';
import { GetPlacesWithBuildingsDto } from './dto/requests/get-places-with-buildings';
import { CountHeader } from 'src/decorators/count-header.decorator';

@ApiTags('places')
@ApiSecurity('API_KEY') // Applies the API key security scheme defined in Swagger
Expand All @@ -35,6 +36,7 @@ export class PlacesController {
) {}

@Get()
@CountHeader()
@ValidateLatLngUser()
@ApiOperation({ summary: 'Get Places using Query params as filters' })
@ApiQuery({type:GetPlacesDto})
Expand All @@ -53,7 +55,8 @@ export class PlacesController {
}

@Get('buildings')
//@ValidateLatLngUser()
@CountHeader()
@ValidateLatLngUser()
@ApiOperation({ summary: 'Get Places with their Building shapes using Query params as filters' })
@ApiQuery({type:GetPlacesWithBuildingsDto})
@ApiResponse({ status: 200, description: 'Return Places with Buildings.' , type: PlaceResponseDto, isArray: true})
Expand All @@ -77,18 +80,20 @@ export class PlacesController {



@Get('brands')
@ValidateLatLngUser()
@ApiOperation({ summary: 'Get all Brands from Places using Query params as filters' })
@ApiResponse({ status: 200, description: 'Return all Brands, along with a count of all Places for each.' , type: BrandDto, isArray: true})
@ApiQuery({type:GetBrandsDto})
@Get('brands')
@CountHeader()
@ValidateLatLngUser()
@ApiOperation({ summary: 'Get all Brands from Places using Query params as filters' })
@ApiResponse({ status: 200, description: 'Return all Brands, along with a count of all Places for each.' , type: BrandDto, isArray: true})
@ApiQuery({type:GetBrandsDto})
async getBrands(@Query() query: GetBrandsDto, @AuthedUser() user: User) {

return await this.placesService.getBrands(query);

}

@Get('countries')
@CountHeader()
@ValidateLatLngUser()
@ApiOperation({ summary: 'Get all Countries from Places using Query params as filters' })
@ApiResponse({ status: 200, description: 'Return all Countries, as well as a count of all the Places and Brands in each.', type:CountryResponseDto, isArray: true})
Expand All @@ -99,11 +104,12 @@ export class PlacesController {
}

@Get('categories')
@CountHeader()
@ValidateLatLngUser()
@ApiOperation({ summary: 'Get all Categories from Places using Query params as filters' })
@ApiResponse({ status: 200, description: 'Return all Categories, along with a count of all Brands and Places for each' , type: CategoryResponseDto, isArray: true})
@ApiQuery({type:GetCategoriesDto})
async getCategories(@Query() query: GetCategoriesDto):Promise<CategoryResponseDto[]> {
async getCategories(@Query() query: GetCategoriesDto, @AuthedUser() user: User):Promise<CategoryResponseDto[]> {

return await this.placesService.getCategories(query);

Expand Down
2 changes: 1 addition & 1 deletion src/places/places.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export class PlacesService {
return cachedResult;
}

const results = await this.bigQueryService.getCategories(query.country);
const results = await this.bigQueryService.getCategories(query.country, query.lat, query.lng, query.radius);

await this.cloudStorageCache.storeJSON (results, cacheKey);
return results.map((category: any) => toCategoryResponseDto(category));
Expand Down

0 comments on commit 6ab2688

Please sign in to comment.