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

feat: add query to look up captures near a particular lat lon #289

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions server/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,24 +64,31 @@ app.use(express.json()); // parse application/json
// routers
app.use('/countries', countriesRouter);
app.use('/v2/countries', countriesRouter);
app.use('/trees', treesRouter);

app.use('/planters', plantersRouter);
app.use('/v2/growers', growerAccountsRouter);

app.use('/organizations', organizationsRouter);
app.use('/v2/organizations', organizationsRouterV2);

app.use('/species', speciesRouter);
app.use('/v2/species', speciesRouterV2);

app.use('/wallets', walletsRouter);
app.use('/v2/wallets', walletsRouter);
app.use('/transactions', transactionsRouter);

app.use('/tokens', tokensRouter);
app.use('/v2/tokens', tokensRouter);

app.use('/trees', treesRouter);
app.use('/v2/trees', treesRouterV2);

app.use('/v2/captures', capturesRouter);
app.use('/raw-captures', rawCapturesRouter);
app.use('/v2/growers', growerAccountsRouter);
app.use('/v2/trees', treesRouterV2);
app.use('/bounds', boundsRouter);
app.use('/gis', gisRouter);
app.use('/contract', contractsRouter);
app.use('/transactions', transactionsRouter);
// Global error handler
app.use(errorHandler);

Expand Down
62 changes: 62 additions & 0 deletions server/infra/database/CaptureRepository.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Capture from 'interfaces/Capture';
import CaptureFilter from 'interfaces/CaptureFilter';
import CaptureLocationFilter from 'interfaces/CaptureLocationFilter';
import FilterOptions from 'interfaces/FilterOptions';
import HttpError from 'utils/HttpError';
import BaseRepository from './BaseRepository';
Expand Down Expand Up @@ -107,6 +108,13 @@ export default class CaptureRepository extends BaseRepository<Capture> {
delete filterObject.organization_id;
}

// remove these filters because they'll be included manually
if (filterObject.lat && filterObject.lon && filterObject.deviation) {
delete filterObject.lat;
delete filterObject.lon;
delete filterObject.deviation;
}

result.where(filterObject);
}

Expand Down Expand Up @@ -184,6 +192,60 @@ export default class CaptureRepository extends BaseRepository<Capture> {
return captures;
}

async getByLocation(
filterCriteria: CaptureLocationFilter,
options: FilterOptions,
) {
const knex = this.session.getDB();
const { sort, ...filter } = filterCriteria;

let promise = knex.select(
knex.raw(
`
treetracker.capture.*,
field_data.device_configuration.device_identifier,
field_data.device_configuration.manufacturer AS device_manufacturer,
treetracker.grower_account.reference_id as grower_reference_id,
treetracker.grower_account.wallet as grower_wallet,
treetracker.grower_account.location as grower_location,
field_data.session.id as session_id
FROM treetracker.capture
LEFT JOIN treetracker.grower_account
ON grower_account.id = treetracker.capture.grower_account_id
LEFT JOIN field_data.device_configuration
ON field_data.device_configuration.id = treetracker.capture.device_configuration_id
LEFT JOIN field_data.raw_capture
ON field_data.raw_capture.id = treetracker.capture.id
LEFT JOIN field_data.session
ON field_data.raw_capture.session_id = field_data.session.id
${
filter.lat && filter.lon && filter.deviation
? `WHERE
ST_DWithin(treetracker.capture.estimated_geometric_location, ST_GeomFromText('POINT (${filter.lon} ${filter.lat})', 4326), ${filter.deviation})
`
: ''
}`,
),
);

promise = promise.orderBy(
sort?.order_by || 'treetracker.capture.created_at',
sort?.order || 'desc',
);

const { limit, offset } = options;
if (limit) {
promise = promise.limit(limit);
}
if (offset) {
promise = promise.offset(offset);
}

const captures = await promise;

return captures;
}

async getCount(filterCriteria: CaptureFilter) {
const knex = this.session.getDB();
const { ...filter } = filterCriteria;
Expand Down
11 changes: 11 additions & 0 deletions server/interfaces/CaptureLocationFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import DbModel from './DbModel';

interface CaptureLocationFilter extends DbModel {
session_id?: Array<string> | undefined;
lat?: string | undefined;
lon?: string | undefined;
deviation?: string | undefined;
sort?: { order?: string; order_by?: string };
}

export default CaptureLocationFilter;
17 changes: 17 additions & 0 deletions server/models/Capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import CaptureRepository from 'infra/database/CaptureRepository';
import { delegateRepository } from 'infra/database/delegateRepository';
import Capture from 'interfaces/Capture';
import CaptureFilter from 'interfaces/CaptureFilter';
import CaptureLocationFilter from 'interfaces/CaptureLocationFilter';
import FilterOptions from 'interfaces/FilterOptions';

function getByFilter(
Expand All @@ -13,6 +14,21 @@ function getByFilter(
};
}

function getByLocation(
captureRepository: CaptureRepository,
): (
filter: CaptureLocationFilter,
options: FilterOptions,
) => Promise<Capture[]> {
return async function (
filter: CaptureLocationFilter,
options: FilterOptions,
) {
const captures = await captureRepository.getByLocation(filter, options);
return captures;
};
}

function getCount(
captureRepository: CaptureRepository,
): (filter: CaptureFilter) => Promise<Capture[]> {
Expand All @@ -24,6 +40,7 @@ function getCount(

export default {
getByFilter,
getByLocation,
getCount,
getById: delegateRepository<CaptureRepository, Capture>('getById'),
};
4 changes: 2 additions & 2 deletions server/models/RawCapture.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import RawCaptureRepository from 'infra/database/CaptureRepository';
import { delegateRepository } from 'infra/database/delegateRepository';
import RawCaptureRepository from 'infra/database/RawCaptureRepository';
import RawCapture from 'interfaces/Capture';
import RawCaptureFilter from 'interfaces/CaptureFilter';
import FilterOptions from 'interfaces/FilterOptions';
Expand All @@ -15,7 +15,7 @@ function getByFilter(

function getCount(
rawCaptureRepository: RawCaptureRepository,
): (filter: RawCaptureFilter) => Promise<{ count: number }> {
): (filter: RawCaptureFilter) => Promise<string | number> {
return async function (filter: RawCaptureFilter) {
const count = await rawCaptureRepository.getCount(filter);
return count;
Expand Down
46 changes: 46 additions & 0 deletions server/routers/capturesRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,52 @@ router.get(
}),
);

router.get(
'/location',
handlerWrapper(async (req, res) => {
const query = queryFormatter(req);

// verify filter values
Joi.assert(
query,
Joi.object().keys({
session_id: Joi.string().uuid(),
lat: Joi.number(),
lon: Joi.number(),
deviation: Joi.number(),
limit: Joi.number().integer().min(1).max(20000),
offset: Joi.number().integer().min(0),
order_by: Joi.string(),
order: Joi.string(),
whereNulls: Joi.array(),
whereNotNulls: Joi.array(),
whereIns: Joi.array(),
}),
);
const {
deviation = '0.01',
limit = 0,
offset = 0,
order = 'desc',
order_by = 'captured_at',
...rest
} = query;

const repo = new CaptureRepository(new Session());
const exe = CaptureModel.getByLocation(repo);
const sort = { order, order_by };
const result = await exe({ ...rest, deviation, sort }, { limit, offset });

res.send({
captures: result,
total: Number(result.length),
offset,
limit,
});
res.end();
}),
);

router.get(
'/:id',
handlerWrapper(async (req, res) => {
Expand Down
Loading