diff --git a/express-api/src/controllers/properties/propertiesSchema.ts b/express-api/src/controllers/properties/propertiesSchema.ts index 5e611e3be..e7b82d7e1 100644 --- a/express-api/src/controllers/properties/propertiesSchema.ts +++ b/express-api/src/controllers/properties/propertiesSchema.ts @@ -33,6 +33,16 @@ export const MapFilterSchema = z.object({ Name: z.string().optional(), RegionalDistrictIds: arrayFromString(numberSchema), UserAgencies: z.array(z.number().int()).optional(), + Polygon: z + .array( + z.array( + z.object({ + x: z.number(), + y: z.number(), + }), + ), + ) + .optional(), }); export type MapFilter = z.infer; diff --git a/express-api/src/routes/propertiesRouter.ts b/express-api/src/routes/propertiesRouter.ts index 31cb538b1..2bd4e8e88 100644 --- a/express-api/src/routes/propertiesRouter.ts +++ b/express-api/src/routes/propertiesRouter.ts @@ -21,6 +21,7 @@ const { router.route('/search/fuzzy').get(userAuthCheck(), catchErrors(getPropertiesFuzzySearch)); router.route('/search/geo').get(userAuthCheck(), catchErrors(getPropertiesForMap)); // Formerly wfs route +// router.route('/search/geo/polygon').get(userAuthCheck(), catchErrors()) // TODO: Write Swagger for this route router.route('/search/linkedProjects').get(userAuthCheck(), catchErrors(getLinkedProjects)); diff --git a/express-api/src/services/properties/propertiesServices.ts b/express-api/src/services/properties/propertiesServices.ts index cf037b0ef..dd6ed64cb 100644 --- a/express-api/src/services/properties/propertiesServices.ts +++ b/express-api/src/services/properties/propertiesServices.ts @@ -30,7 +30,7 @@ import { constructFindOptionFromQuerySingleSelect, } from '@/utilities/helperFunctions'; import userServices from '../users/usersServices'; -import { Brackets, FindOptionsWhere, ILike, In, QueryRunner } from 'typeorm'; +import { Brackets, FindOptionsWhere, ILike, In, QueryRunner, Raw } from 'typeorm'; import { PropertyType } from '@/constants/propertyType'; import { exposedProjectStatuses, ProjectStatus } from '@/constants/projectStatus'; import { ProjectProperty } from '@/typeorm/Entities/ProjectProperty'; @@ -38,6 +38,7 @@ import { ProjectStatus as ProjectStatusEntity } from '@/typeorm/Entities/Project import { parentPort } from 'worker_threads'; import { ErrorWithCode } from '@/utilities/customErrors/ErrorWithCode'; import { PimsRequestUser } from '@/middleware/userAuthCheck'; +import { isPointInMultiPolygon } from '@/utilities/polygonMath'; /** * Perform a fuzzy search for properties based on the provided keyword. @@ -211,6 +212,7 @@ const getPropertiesForMap = async (filter?: MapFilter) => { Name: filter.Name ? ILike(`%${filter.Name}%`) : undefined, PropertyTypeId: filter.PropertyTypeIds ? In(filter.PropertyTypeIds) : undefined, RegionalDistrictId: filter.RegionalDistrictIds ? In(filter.RegionalDistrictIds) : undefined, + Location: filter.Polygon ? Raw(``) : undefined, }; /** @@ -244,6 +246,11 @@ const getPropertiesForMap = async (filter?: MapFilter) => { }, ], }); + + if (filter.Polygon) + return properties.filter((property) => + isPointInMultiPolygon(property.Location, filter.Polygon as { x: number; y: number }[][]), + ); return properties; } /** diff --git a/express-api/src/utilities/polygonMath.ts b/express-api/src/utilities/polygonMath.ts new file mode 100644 index 000000000..e0d0a8b3a --- /dev/null +++ b/express-api/src/utilities/polygonMath.ts @@ -0,0 +1,45 @@ +/** + * Checks if a point is inside a polygon using the Ray-Casting algorithm. + * @param point The point to check. + * @param polygonPoints The polygon defined as an array of points. + * @returns True if the point is inside the polygon, false otherwise. + */ +export const isPointInPolygon = ( + point: { x: number; y: number }, + polygonPoints: { x: number; y: number }[], +): boolean => { + let isInside = false; + const { x, y } = point; + const n = polygonPoints.length; + + for (let i = 0, j = n - 1; i < n; j = i++) { + const xi = polygonPoints[i].x, + yi = polygonPoints[i].y; + const xj = polygonPoints[j].x, + yj = polygonPoints[j].y; + + const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi; + + if (intersect) isInside = !isInside; + } + + return isInside; +}; + +/** + * Checks if a point is inside any of the polygons in a multipolygon. + * @param point The point to check. + * @param multiPolygon The multipolygon, an array of polygons. + * @returns True if the point is inside any polygon, false otherwise. + */ +export const isPointInMultiPolygon = ( + point: { x: number; y: number }, + multiPolygon: { x: number; y: number }[][], +): boolean => { + for (const polygon of multiPolygon) { + if (isPointInPolygon(point, polygon)) { + return true; + } + } + return false; +};