Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pratishta/radius filter #1500

Merged
merged 9 commits into from
May 16, 2024
4 changes: 4 additions & 0 deletions client/app/controllers/query-parameters/show-geography.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ export const projectParams = new QueryParams({
defaultValue: '',
refresh: true,
},
blocks_in_radius: {
defaultValue: [],
refresh: true,
},
dcp_ulurp_nonulurp: {
defaultValue: [],
refresh: true,
Expand Down
9 changes: 9 additions & 0 deletions client/tests/acceptance/filter-checkbox-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ module('Acceptance | filter checkbox', function(hooks) {
assert.equal(currentURL().includes('dcp_femafloodzonea=true'), true);
});

test('User clicks radius filter checkbox', async function(assert) {
server.createList('project', 20);
await visit('/');
await click('[data-test-filter-section="filter-section-radius-filter"] .switch-paddle');

assert.equal(currentURL().includes('distance_from_point'), true);
assert.equal(currentURL().includes('radius_from_point'), true);
});

test('User clicks community district box, fills in community district name, selects CD', async function(assert) {
server.createList('project', 20);
await visit('/');
Expand Down
28 changes: 27 additions & 1 deletion server/src/crm/crm.utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,33 @@ export function containsAnyOf(propertyName, strings = [], options?) {
return `(${not ? 'not ' : ''}${lambdaQueryPrefix}(${containsQuery}))`;
}

export const dateParser = function(key, value) {
export function startsWithString(propertyName, string) {
return `startswith(${propertyName}, '${string}')`;
}

export function startsWithAnyOf(propertyName, strings = [], options?) {
const {
childEntity = '',
comparisonStrategy = startsWithString,
not = false,
} = options || {};

const containsQuery = strings
.map((string, i) => {
// in odata syntax, this character o is a variable for scoping
// logic for related entities. it needs to only appear once.
const lambdaScope = (childEntity && i === 0) ? `${childEntity}:` : '';
const lambdaScopedProperty = childEntity ? `${childEntity}/${propertyName}` : propertyName;

return `${lambdaScope}${comparisonStrategy(lambdaScopedProperty, string)}`;
})
.join(' or ');
const lambdaQueryPrefix = childEntity ? `${childEntity}/any` : '';

return `(${not ? 'not ' : ''}${lambdaQueryPrefix}(${containsQuery}))`;
}

export const dateParser = function (key, value) {
if (typeof value === 'string') {
// YYYY-MM-DDTHH:mm:ss.sssZ => parsed as UTC
// YYYY-MM-DD => parsed as local date
Expand Down
9 changes: 7 additions & 2 deletions server/src/project/geometry/geometry.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ const QUERY_TEMPLATES = {
"dcp_project"
),

blocks_in_radius: (queryParamValue) =>
containsString('dcp_bblnumber', [queryParamValue], 'dcp_projectbbl'),

dcp_femafloodzonev: queryParamValue =>
comparisonOperator(
"dcp_femafloodzonev",
Expand Down Expand Up @@ -360,8 +363,10 @@ export class GeometryService {
const blocks = await this.carto.fetchCarto(distinctBlocks, "json", "post");

// note: DTM stores blocks with the borough
return blocks.map(block => `${block.block.substring(1)}`);
}

return blocks
.map(block => `${block.block}`);
}

// Warning! Returns either null or an Object
async getBblsGeometry(bbls = []) {
Expand Down
55 changes: 27 additions & 28 deletions server/src/project/project.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import {
containsString,
equalsAnyOf,
containsAnyOf,
startsWithAnyOf,
startsWithString,
overwriteCodesWithLabels
} from "../crm/crm.utilities";
import { ArtifactService } from "../artifact/artifact.service";
Expand Down Expand Up @@ -214,8 +216,9 @@ const QUERY_TEMPLATES = {
}
)
),
blocks_in_radius: queryParamValue =>
containsAnyOf("dcp_validatedblock", queryParamValue, {

blocks_in_radius: queryParamValue =>
startsWithAnyOf("dcp_bblnumber", queryParamValue, {
childEntity: "dcp_dcp_project_dcp_projectbbl_project"
})
};
Expand All @@ -232,11 +235,11 @@ export const ALLOWED_FILTERS = [
"dcp_publicstatus", // 'Noticed', 'Filed', 'In Public Review', 'Completed', 'Unknown'
"dcp_certifiedreferred",
"project_applicant_text",
"block",
"distance_from_point",
"radius_from_point",
"zoning-resolutions",
"dcp_applicability"
"dcp_applicability",
"blocks_in_radius",
];

export const generateFromTemplate = (query, template) => {
Expand All @@ -247,11 +250,6 @@ export const generateFromTemplate = (query, template) => {
};

function generateProjectsFilterString(query) {
// Special handling for 'block' query, which must be explicitly ignored if empty
// otherwise, unmapped projects will be excluded from the results
if (!query.block) delete query.block;

// optional params
// apply only those that appear in the query object
const requestedFiltersQuery = generateFromTemplate(query, QUERY_TEMPLATES);
return all(
Expand All @@ -263,7 +261,7 @@ function generateProjectsFilterString(query) {
),
// optional params
...requestedFiltersQuery
);
);;
}

function generateQueryObject(query, overrides?) {
Expand Down Expand Up @@ -583,11 +581,7 @@ export class ProjectService {
return this.serialize(transformedProject);
}

async blocksWithinRadius(query) {
let { distance_from_point, radius_from_point } = query;

if (!distance_from_point || !radius_from_point) return {};

async blocksWithinRadius(distance_from_point, radius_from_point) {
// search cannot support more than 1000 because of URI Too Large errors
// if (radius_from_point > 1000) radius_from_point = 1000;

Expand All @@ -601,31 +595,36 @@ export class ProjectService {
}

async queryProjects(query, itemsPerPage = ITEMS_PER_PAGE) {
const blocks = await this.blocksWithinRadius(query);

// adds in the blocks filter for use across various query types
const normalizedQuery = {
const radiusFilterOn = query.radius_from_point && query.distance_from_point;
const blocks = radiusFilterOn ? await this.blocksWithinRadius(query.distance_from_point, query.radius_from_point) : [];

const normalizedQuery = blocks.length == 0 ? {...query} : {
blocks_in_radius: blocks,
...query

// this information is sent as separate filters but must be represented as one
// to work correctly with the query template system.
// ...blocks
};

const queryObject = generateQueryObject(normalizedQuery);
const spatialInfo = await this.geometryService.createAnonymousMapWithFilters(
normalizedQuery
);

// Return empty projects when radius filter is on and Carto returns 0 blocks
// otherwise, send the OData query
const {
records: projects,
skipTokenParams: nextPageSkipTokenParams,
count
} = await this.crmService.queryFromObject(
"dcp_projects",
queryObject,
itemsPerPage
);
} = blocks.length == 0 && radiusFilterOn ?
{
records: [],
skipTokenParams: undefined,
count: 0
} :
await this.crmService.queryFromObject(
"dcp_projects",
queryObject,
itemsPerPage
);

const valueMappedRecords = overwriteCodesWithLabels(
projects,
Expand Down
Loading